save
This commit is contained in:
parent
9b15ac9fd3
commit
27df1a73b5
28 changed files with 1695 additions and 247 deletions
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
namespace app;
|
||||
// for email
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
@ -7,6 +8,7 @@ use PHPMailer\PHPMailer\Exception;
|
|||
class app
|
||||
{
|
||||
public static $db;
|
||||
|
||||
public static function init_db()
|
||||
{
|
||||
try {
|
||||
|
@ -16,6 +18,7 @@ class app
|
|||
die("Database error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function send_mail($to, $from, $from_name, $subject, $message, $HTML_message)
|
||||
{
|
||||
$mail = new PHPMailer(exceptions: true);
|
||||
|
@ -40,6 +43,7 @@ class app
|
|||
$mail->send();
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
public static function sendJson($data, $status = 200)
|
||||
{
|
||||
http_response_code($status);
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace app\controllers;
|
|||
use app\models\addresses;
|
||||
use app\models\users;
|
||||
use app\models\user_addresses;
|
||||
use app\models\magic_links;
|
||||
|
||||
class account
|
||||
{
|
||||
|
@ -12,8 +13,8 @@ class account
|
|||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: /account/login');
|
||||
}
|
||||
$email = $_SESSION['user_email'];
|
||||
$user = users::getByEmail($email);
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$user = users::getById($user_id);
|
||||
$default_shipping = null;
|
||||
$default_billing = null;
|
||||
$ship_addrs = [];
|
||||
|
@ -60,8 +61,8 @@ class account
|
|||
$bill_id = addresses::add(
|
||||
$bill['name'],
|
||||
$bill['company'],
|
||||
$bill['street'],
|
||||
$bill['boxapt'],
|
||||
$bill['addressLine1'],
|
||||
$bill['addressLine2'],
|
||||
$bill['city'],
|
||||
$bill['state'],
|
||||
$bill['zip'],
|
||||
|
@ -76,8 +77,8 @@ class account
|
|||
$_SESSION['success'] = "Billing address saved!";
|
||||
header('Location: /account/billing');
|
||||
}
|
||||
$email = $_SESSION['user_email'];
|
||||
$user = users::getByEmail($email);
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$user = users::getById($user_id);
|
||||
$default_billing = null;
|
||||
$bill_addrs = [];
|
||||
$bill_addresses = user_addresses::getBillingByUserId($_SESSION['user_id']);
|
||||
|
@ -115,8 +116,42 @@ class account
|
|||
header('Location: /account');
|
||||
}
|
||||
}
|
||||
|
||||
public static function email()
|
||||
{
|
||||
$user_id = $_SESSION['user_id'] ?? null;
|
||||
if (empty($user_id)){
|
||||
header('Location: /account/login');
|
||||
}
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$email = $_POST['email'] ?? null;
|
||||
if (empty($email)) {
|
||||
$_SESSION['error'] = "Enter your email to get a login link";
|
||||
header('Location: /account');
|
||||
exit;
|
||||
} else {
|
||||
$token = magic_links::add($email, $user_id);
|
||||
users::updateReplaceEmailTokenById($user_id, $token);
|
||||
header('Location: /account');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function login($defaults)
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$email = $_POST['email'] ?? false;
|
||||
if (empty($email)) {
|
||||
$_SESSION['error'] = "Enter your email to get a login link";
|
||||
header('Location: /account/login');
|
||||
exit;
|
||||
} else {
|
||||
$token = magic_links::add($email, null);
|
||||
header('Location: /account/login');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
header('Location: /account');
|
||||
}
|
||||
|
@ -131,12 +166,14 @@ class account
|
|||
]
|
||||
]));
|
||||
}
|
||||
|
||||
public static function logout()
|
||||
{
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header('Location: /');
|
||||
}
|
||||
|
||||
public static function orders($defaults)
|
||||
{
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
|
@ -185,8 +222,8 @@ class account
|
|||
$ship_id = addresses::add(
|
||||
$ship['name'],
|
||||
$ship['company'],
|
||||
$ship['street'],
|
||||
$ship['boxapt'],
|
||||
$ship['addressLine1'],
|
||||
$ship['addressLine2'],
|
||||
$ship['city'],
|
||||
$ship['state'],
|
||||
$ship['zip'],
|
||||
|
@ -201,8 +238,8 @@ class account
|
|||
$_SESSION['success'] = "Shipping address saved!";
|
||||
header('Location: /account/shipping');
|
||||
}
|
||||
$email = $_SESSION['user_email'];
|
||||
$user = users::getByEmail($email);
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$user = users::getById($user_id);
|
||||
$addresses = user_addresses::getShippingByUserId($user['id']);
|
||||
$default_shipping = null;
|
||||
$ship_addrs = [];
|
||||
|
@ -235,27 +272,30 @@ class account
|
|||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$email = $_POST['email'];
|
||||
if (empty($email)) {
|
||||
$_SESSION['error'] = 'Email is required.';
|
||||
}
|
||||
$existingUser = users::getByEmail($email);
|
||||
if ($existingUser) {
|
||||
$_SESSION['error'] = 'Email already exists. Please choose a different email or log in.';
|
||||
$_SESSION['last_post'] = $_POST;
|
||||
header('Location: /account/signup');
|
||||
exit;
|
||||
}
|
||||
if (empty($email)) {
|
||||
$_SESSION['error'] = 'Email is required.';
|
||||
}
|
||||
if (isset($_SESSION['error'])) {
|
||||
$useShipping = $_POST['use_shipping'] ?? false;
|
||||
$ship = addresses::validatePost("shipping");
|
||||
if (!isset($ship['name'])){
|
||||
$_SESSION['error'] = "Shipping address verification failed. Check your entry for errors.";
|
||||
$_SESSION['last_post'] = $_POST;
|
||||
header('Location: /account/signup');
|
||||
}
|
||||
$useShipping = $_POST['use_shipping'] ?? false;
|
||||
if ($useShipping) {
|
||||
$ship = addresses::validatePost("shipping");
|
||||
} else {
|
||||
$ship = addresses::validatePost("shipping");
|
||||
if (!$useShipping) {
|
||||
$bill = addresses::validatePost("billing");
|
||||
}
|
||||
if (empty($email)) {
|
||||
$_SESSION['error'] = 'Email is required.';
|
||||
if (!isset($bill['name'])){
|
||||
$_SESSION['error'] = "Billing address verification failed. Check your entry for errors.";
|
||||
$_SESSION['last_post'] = $_POST;
|
||||
header('Location: /account/signup');
|
||||
}
|
||||
}
|
||||
if (isset($_SESSION['error'])) {
|
||||
$_SESSION['last_post'] = $_POST;
|
||||
|
@ -264,8 +304,8 @@ class account
|
|||
$ship_id = addresses::add(
|
||||
$ship['name'],
|
||||
$ship['company'],
|
||||
$ship['street'],
|
||||
$ship['boxapt'],
|
||||
$ship['addressLine1'],
|
||||
$ship['addressLine2'],
|
||||
$ship['city'],
|
||||
$ship['state'],
|
||||
$ship['zip'],
|
||||
|
@ -278,8 +318,8 @@ class account
|
|||
$bill_id = addresses::add(
|
||||
$bill['name'],
|
||||
$bill['company'],
|
||||
$bill['street'],
|
||||
$bill['boxapt'],
|
||||
$bill['addressLine1'],
|
||||
$bill['addressLine2'],
|
||||
$bill['city'],
|
||||
$bill['state'],
|
||||
$bill['zip'],
|
||||
|
|
|
@ -8,60 +8,45 @@ class magic_link
|
|||
{
|
||||
public static function index()
|
||||
{
|
||||
$email = $_GET['email'] ?? null;
|
||||
$token = $_GET['token'] ?? null;
|
||||
$signup = $_GET['signup'] ?? null;
|
||||
|
||||
if (empty($email) && empty($token)) {
|
||||
$_SESSION['error'] = "Enter your email to get a login link";
|
||||
if (!$token) {
|
||||
$_SESSION['error'] = "Invalid or expired link.";
|
||||
header('Location: /account/login');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($email && empty($token) && empty($signup)) {
|
||||
$link = magic_links::add(email: $email);
|
||||
$subject = "Your Magic Sign-In Link";
|
||||
$message = "Copy and paste the link into your browser: $link";
|
||||
$HTML_message = "Click the link to sign in: <a href='$link'>$link</a>";
|
||||
app::send_mail(to: $email, from: $_ENV['SMTP_FROM'], from_name: $_ENV['APP_NAME'], subject: $subject, message: $message, HTML_message: $HTML_message);
|
||||
$_SESSION['success'] = 'Link sent to your email!';
|
||||
header('Location: /account/login');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($email && empty($token) && $signup == "1") {
|
||||
$link = magic_links::add(email: $email);
|
||||
$subject = "Your Magic Sign-In Link";
|
||||
$message = "Copy and paste the link into your browser: $link";
|
||||
$HTML_message = "Click the link to sign in: <a href='$link'>$link</a>";
|
||||
app::send_mail(to: $email, from: $_ENV['SMTP_FROM'], from_name: $_ENV['APP_NAME'], subject: $subject, message: $message, HTML_message: $HTML_message);
|
||||
$_SESSION['success'] = 'Account created! Please check your email inbox for the verification link.';
|
||||
header('Location: /account/login');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($token && empty($email)) {
|
||||
$link = magic_links::validate(token: $token);
|
||||
|
||||
} else {
|
||||
$link = magic_links::validateToken(token: $token);
|
||||
if (!$link) {
|
||||
$_SESSION['error'] = "Invalid or expired link.";
|
||||
header('Location: /account/login');
|
||||
exit;
|
||||
}
|
||||
// handle signup vs. login
|
||||
$user = users::getByEmail($link['email']);
|
||||
if ($user) {
|
||||
$user = $link['user_id'] ? users::getById($link['user_id']) : users::getByEmail($link['email']);
|
||||
if ($user) { // user with this email exists, log them in
|
||||
$_SESSION['user_email'] = $link['email'];
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
if (!$user['verified']) {
|
||||
users::verify($link['email']);
|
||||
}
|
||||
header('Location: /account');
|
||||
} else {
|
||||
// used to pre-fill email signup field
|
||||
$_SESSION['user_email'] = $link['email'];
|
||||
header('Location: /account/signup');
|
||||
exit;
|
||||
} else { // no users with this email
|
||||
$user_replacing_email = users::getByReplaceEmailToken($token);
|
||||
if ($user_replacing_email) { // user is replacing their email
|
||||
$user_id = $user_replacing_email['id'];
|
||||
users::updateEmailById($user_id, $link['email']);
|
||||
$_SESSION['user_email'] = $link['email'];
|
||||
$_SESSION['user_id'] = $user_id;
|
||||
if (!$user['verified']) {
|
||||
users::verify($link['email']);
|
||||
}
|
||||
header('Location: /account');
|
||||
exit;
|
||||
} else { // new user signup
|
||||
$_SESSION['user_email'] = $link['email'];
|
||||
header('Location: /account/signup');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ class addresses
|
|||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
company TEXT,
|
||||
street TEXT NOT NULL,
|
||||
boxapt TEXT NOT NULL,
|
||||
addressLine1 TEXT NOT NULL,
|
||||
addressLine2 TEXT NOT NULL,
|
||||
city TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
zip TEXT NOT NULL,
|
||||
|
@ -25,36 +25,52 @@ class addresses
|
|||
{
|
||||
$name = $_POST["{$type}_name"];
|
||||
$company = $_POST["{$type}_company"] ?? null;
|
||||
$boxapt = $_POST["{$type}_boxapt"] ?? null;
|
||||
$street = $_POST["{$type}_street"];
|
||||
$addressLine2 = $_POST["{$type}_addressLine2"] ?? null;
|
||||
$addressLine1 = $_POST["{$type}_addressLine1"];
|
||||
$city = $_POST["{$type}_city"];
|
||||
$state = $_POST["{$type}_state"];
|
||||
$zip = $_POST["{$type}_zip"];
|
||||
$phone = $_POST["{$type}_phone"];
|
||||
// check all required fields are set
|
||||
if (empty($name) || empty($street) || empty($city) || empty($state) || empty($zip) || empty($phone)) {
|
||||
if (empty($name) || empty($addressLine1) || empty($city) || empty($state) || empty($zip) || empty($phone)) {
|
||||
$_SESSION['error'] = "Missing required {$type} information.";
|
||||
}
|
||||
// TODO: find a match using postal database and return that
|
||||
$url = "https://nominatim.openstreetmap.org/search?" . http_build_query([
|
||||
"q" => implode(" ", array_filter([$addressLine1, $addressLine2, $city, $state, $zip])),
|
||||
"format" => "json",
|
||||
"addressdetails" => 1,
|
||||
"limit" => 1
|
||||
]);
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, "AddressValidator/1.0");
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
$data = json_decode($response, true);
|
||||
if (empty($data)) {
|
||||
return ["error" => "Address not found"];
|
||||
}
|
||||
$addressDetails = $data[0]['address'];
|
||||
return [
|
||||
'name' => $name,
|
||||
'company' => $company,
|
||||
'street' => $street,
|
||||
'boxapt' => $boxapt,
|
||||
'city' => $city,
|
||||
'state' => $state,
|
||||
'zip' => $zip,
|
||||
'phone' => $phone
|
||||
'name' => $name,
|
||||
'company' => $company,
|
||||
"addressLine1" => ($addressDetails['house_number'] ?? '') . ' ' . ($addressDetails['building'] ?? '') . ' ' . ($addressDetails['road'] ?? ''),
|
||||
"addressLine2" => $addressLine2,
|
||||
"city" => $addressDetails['city'] ?? $addressDetails['town'] ?? $addressDetails['village'] ?? '',
|
||||
"state" => $addressDetails['state_code'] ?? ($addressDetails['state'] ?? ''),
|
||||
"zip" => $addressDetails['postcode'] ?? '',
|
||||
'phone' => $phone
|
||||
];
|
||||
}
|
||||
|
||||
public static function add($name, $company, $street, $boxapt, $city, $state, $zip, $phone, $billing, $shipping)
|
||||
public static function add($name, $company, $addressLine1, $addressLine2, $city, $state, $zip, $phone, $billing, $shipping)
|
||||
{
|
||||
$query = "INSERT INTO addresses (
|
||||
name,
|
||||
company,
|
||||
street,
|
||||
boxapt,
|
||||
addressLine1,
|
||||
addressLine2,
|
||||
city,
|
||||
state,
|
||||
zip,
|
||||
|
@ -64,8 +80,8 @@ class addresses
|
|||
) VALUES (
|
||||
:name,
|
||||
:company,
|
||||
:street,
|
||||
:boxapt,
|
||||
:addressLine1,
|
||||
:addressLine2,
|
||||
:city,
|
||||
:state,
|
||||
:zip,
|
||||
|
@ -76,8 +92,8 @@ class addresses
|
|||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':name', $name);
|
||||
$stmt->bindParam(':company', $company);
|
||||
$stmt->bindParam(':street', $street);
|
||||
$stmt->bindParam(':boxapt', $boxapt);
|
||||
$stmt->bindParam(':addressLine1', $addressLine1);
|
||||
$stmt->bindParam(':addressLine2', $addressLine2);
|
||||
$stmt->bindParam(':city', $city);
|
||||
$stmt->bindParam(':state', $state);
|
||||
$stmt->bindParam(':zip', $zip);
|
||||
|
|
53
src/models/cart_items.php
Normal file
53
src/models/cart_items.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
namespace app\models;
|
||||
|
||||
use app\app;
|
||||
|
||||
class cart_items
|
||||
{
|
||||
public static function init()
|
||||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS cart_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cart_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
quantity INTEGER NOT NULL CHECK(quantity > 0),
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (cart_id) REFERENCES carts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
);");
|
||||
}
|
||||
|
||||
public static function addItem(int $cartId, int $productId, int $quantity)
|
||||
{
|
||||
$stmt = app::$db->prepare("INSERT INTO cart_items (cart_id, product_id, quantity)
|
||||
VALUES (:cart_id, :product_id, :quantity)");
|
||||
$stmt->execute([
|
||||
'cart_id' => $cartId,
|
||||
'product_id' => $productId,
|
||||
'quantity' => $quantity
|
||||
]);
|
||||
}
|
||||
|
||||
public static function updateItem(int $cartItemId, int $quantity)
|
||||
{
|
||||
$stmt = app::$db->prepare("UPDATE cart_items SET quantity = :quantity WHERE cart_item_id = :cart_item_id");
|
||||
$stmt->execute([
|
||||
'cart_item_id' => $cartItemId,
|
||||
'quantity' => $quantity
|
||||
]);
|
||||
}
|
||||
|
||||
public static function removeItem(int $cartItemId)
|
||||
{
|
||||
$stmt = app::$db->prepare("DELETE FROM cart_items WHERE cart_item_id = :cart_item_id");
|
||||
$stmt->execute(['cart_item_id' => $cartItemId]);
|
||||
}
|
||||
|
||||
public static function getCartItems(int $cartId): array
|
||||
{
|
||||
$stmt = app::$db->prepare("SELECT * FROM cart_items WHERE cart_id = :cart_id");
|
||||
$stmt->execute(['cart_id' => $cartId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
}
|
|
@ -2,20 +2,42 @@
|
|||
namespace app\models;
|
||||
|
||||
use app\app;
|
||||
|
||||
class carts
|
||||
{
|
||||
public static function init()
|
||||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS order_items (
|
||||
order_item_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
quantity INTEGER NOT NULL CHECK(quantity > 0),
|
||||
price REAL NOT NULL CHECK(price >= 0),
|
||||
FOREIGN KEY (order_id) REFERENCES orders(order_id),
|
||||
FOREIGN KEY (product_id) REFERENCES products(product_id)
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS carts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
short_id TEXT UNIQUE NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);");
|
||||
}
|
||||
|
||||
public static function addCart(int $userId): string
|
||||
{
|
||||
return self::createCart($userId, 'user_cart_id');
|
||||
}
|
||||
|
||||
public static function addSaved(int $userId): string
|
||||
{
|
||||
return self::createCart($userId, 'user_saved_for_later_id');
|
||||
}
|
||||
|
||||
private static function createCart(int $userId, string $column): string
|
||||
{
|
||||
$characters = '123456789ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||||
$shortId = '';
|
||||
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
$shortId .= $characters[random_int(0, strlen($characters) - 1)];
|
||||
}
|
||||
|
||||
app::$db->prepare("INSERT INTO carts (user_id, short_id) VALUES (:user_id, :short_id)")
|
||||
->execute(['user_id' => $userId, 'short_id' => $shortId]);
|
||||
|
||||
return $shortId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -8,38 +8,84 @@ class magic_links
|
|||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS magic_links (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
email TEXT NOT NULL,
|
||||
code TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
used BOOLEAN DEFAULT FALSE
|
||||
)");
|
||||
}
|
||||
|
||||
public static function add($email)
|
||||
public static function add($email, $user_id)
|
||||
{
|
||||
$code = str_pad(strval(random_int(0, 999999)), 6, "0", STR_PAD_LEFT);
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expires_at = date('Y-m-d H:i:s', time() + 60 * 15);
|
||||
app::$db->query("INSERT INTO magic_links (
|
||||
email,
|
||||
token,
|
||||
$query = "INSERT INTO magic_links (
|
||||
email,
|
||||
user_id,
|
||||
token,
|
||||
code,
|
||||
expires_at
|
||||
) VALUES (
|
||||
'$email',
|
||||
'$token',
|
||||
'$expires_at'
|
||||
)");
|
||||
return $_ENV['APP_HOST'] . "/magic-link?token=" . urlencode($token);
|
||||
:email,
|
||||
:user_id,
|
||||
:token,
|
||||
:code,
|
||||
:expires_at
|
||||
)";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':email', $email);
|
||||
$stmt->bindParam(':user_id', $user_id);
|
||||
$stmt->bindParam(':token', $token);
|
||||
$stmt->bindParam(':code', $code);
|
||||
$stmt->bindParam(':expires_at', $expires_at);
|
||||
$stmt->execute();
|
||||
$link = $_ENV['APP_HOST'] . "/magic-link?token=" . urlencode($token);
|
||||
$subject = "Your Magic Sign-In Link";
|
||||
$message = "Enter this code into the sign-in form\n$code\n or copy-paste this link into your browser to sign in:\n$link";
|
||||
$HTML_message = "Click the link to sign in: <a href='$link'>$link</a> or enter this code:<br/>$code";
|
||||
app::send_mail(to: $email, from: $_ENV['SMTP_FROM'], from_name: $_ENV['APP_NAME'], subject: $subject, message: $message, HTML_message: $HTML_message);
|
||||
$_SESSION['success'] = 'Link sent to your email!';
|
||||
return $token;
|
||||
}
|
||||
|
||||
public static function validate($token)
|
||||
public static function validateToken($token)
|
||||
{
|
||||
$link = app::$db->query("SELECT * FROM magic_links
|
||||
WHERE token = '$token'
|
||||
AND used = FALSE
|
||||
AND expires_at > datetime('now')
|
||||
")->fetch(\PDO::FETCH_ASSOC);
|
||||
// void the token once validated
|
||||
app::$db->query("UPDATE magic_links SET used = TRUE WHERE token = '$token'");
|
||||
$query = "SELECT * FROM magic_links
|
||||
WHERE token = :token
|
||||
AND used = FALSE
|
||||
AND expires_at > datetime('now')";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':token', $token);
|
||||
$stmt->execute();
|
||||
$link = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
$updateQuery = "UPDATE magic_links SET used = TRUE WHERE token = :token";
|
||||
$updateStmt = app::$db->prepare($updateQuery);
|
||||
$updateStmt->bindParam(':token', $token);
|
||||
$updateStmt->execute();
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
public static function validateCode($code)
|
||||
{
|
||||
$query = "SELECT * FROM magic_links
|
||||
WHERE code = :code
|
||||
AND used = FALSE
|
||||
AND expires_at > datetime('now')";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':code', $code);
|
||||
$stmt->execute();
|
||||
$link = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
$updateQuery = "UPDATE magic_links SET used = TRUE WHERE code = :code";
|
||||
$updateStmt = app::$db->prepare($updateQuery);
|
||||
$updateStmt->bindParam(':code', $code);
|
||||
$updateStmt->execute();
|
||||
|
||||
return $link;
|
||||
}
|
||||
}
|
||||
|
|
61
src/models/order_items.php
Normal file
61
src/models/order_items.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
namespace app\models;
|
||||
|
||||
use app\app;
|
||||
|
||||
class order_items
|
||||
{
|
||||
public static function init()
|
||||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS order_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
quantity INTEGER NOT NULL CHECK(quantity > 0),
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
);");
|
||||
}
|
||||
|
||||
public static function addItem(int $orderId, int $productId, int $quantity)
|
||||
{
|
||||
if ($quantity <= 0) {
|
||||
throw new \InvalidArgumentException('Quantity must be greater than zero.');
|
||||
}
|
||||
|
||||
$stmt = app::$db->prepare("INSERT INTO order_items (order_id, product_id, quantity)
|
||||
VALUES (:order_id, :product_id, :quantity)");
|
||||
$stmt->execute([
|
||||
'order_id' => $orderId,
|
||||
'product_id' => $productId,
|
||||
'quantity' => $quantity
|
||||
]);
|
||||
}
|
||||
|
||||
public static function updateItem(int $orderItemId, int $quantity)
|
||||
{
|
||||
if ($quantity <= 0) {
|
||||
throw new \InvalidArgumentException('Quantity must be greater than zero.');
|
||||
}
|
||||
|
||||
$stmt = app::$db->prepare("UPDATE order_items SET quantity = :quantity WHERE order_item_id = :order_item_id");
|
||||
$stmt->execute([
|
||||
'order_item_id' => $orderItemId,
|
||||
'quantity' => $quantity
|
||||
]);
|
||||
}
|
||||
|
||||
public static function removeItem(int $orderItemId)
|
||||
{
|
||||
$stmt = app::$db->prepare("DELETE FROM order_items WHERE order_item_id = :order_item_id");
|
||||
$stmt->execute(['order_item_id' => $orderItemId]);
|
||||
}
|
||||
|
||||
public static function getOrderItems(int $orderId): array
|
||||
{
|
||||
$stmt = app::$db->prepare("SELECT * FROM order_items WHERE order_id = :order_id");
|
||||
$stmt->execute(['order_id' => $orderId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
}
|
|
@ -2,17 +2,72 @@
|
|||
namespace app\models;
|
||||
|
||||
use app\app;
|
||||
|
||||
class orders
|
||||
{
|
||||
const STATUSES = [
|
||||
'SHIPPED', 'PENDING', 'HOLD', 'PARTIAL',
|
||||
'BACKORDER', 'FAILED', 'CANCELED', 'PROCESSING'
|
||||
];
|
||||
|
||||
public static function init()
|
||||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS orders (
|
||||
order_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
total_amount REAL NOT NULL CHECK(total_amount >= 0),
|
||||
status TEXT NOT NULL CHECK(status IN ('pending', 'completed', 'cancelled')),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(user_id)
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
value_sats INTEGER NOT NULL CHECK(value_sats >= 0),
|
||||
value_cents INTEGER NOT NULL CHECK(value_cents >= 0),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT CHECK(status IN ('" . implode("', '", self::STATUSES) . "')) NOT NULL DEFAULT 'PENDING',
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);");
|
||||
}
|
||||
|
||||
public static function createOrder(int $userId, int $valueSats, int $valueCents, string $status = 'PENDING'): int
|
||||
{
|
||||
self::validateStatus($status);
|
||||
|
||||
$stmt = app::$db->prepare("INSERT INTO orders (user_id, value_sats, value_cents, status)
|
||||
VALUES (:user_id, :value_sats, :value_cents, :status)");
|
||||
$stmt->execute([
|
||||
'user_id' => $userId,
|
||||
'value_sats' => $valueSats,
|
||||
'value_cents' => $valueCents,
|
||||
'status' => $status
|
||||
]);
|
||||
|
||||
return app::$db->lastInsertId();
|
||||
}
|
||||
|
||||
public static function updateStatus(int $orderId, string $status)
|
||||
{
|
||||
self::validateStatus($status);
|
||||
|
||||
$stmt = app::$db->prepare("UPDATE orders SET status = :status WHERE order_id = :order_id");
|
||||
$stmt->execute([
|
||||
'order_id' => $orderId,
|
||||
'status' => $status
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getOrder(int $orderId): array
|
||||
{
|
||||
$stmt = app::$db->prepare("SELECT * FROM orders WHERE order_id = :order_id");
|
||||
$stmt->execute(['order_id' => $orderId]);
|
||||
return $stmt->fetch() ?: [];
|
||||
}
|
||||
|
||||
public static function getUserOrders(int $userId): array
|
||||
{
|
||||
$stmt = app::$db->prepare("SELECT * FROM orders WHERE user_id = :user_id ORDER BY created_at DESC");
|
||||
$stmt->execute(['user_id' => $userId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
private static function validateStatus(string $status)
|
||||
{
|
||||
if (!in_array($status, self::STATUSES, true)) {
|
||||
throw new \InvalidArgumentException("Invalid order status: $status");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,16 +2,69 @@
|
|||
namespace app\models;
|
||||
|
||||
use app\app;
|
||||
|
||||
class products
|
||||
{
|
||||
public static function init()
|
||||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS products (
|
||||
product_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
price REAL NOT NULL CHECK(price >= 0),
|
||||
qty INTEGER NOT NULL DEFAULT 0 CHECK(qty >= 0)
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
desc TEXT,
|
||||
stock_qty INTEGER NOT NULL DEFAULT 0 CHECK(stock_qty >= 0),
|
||||
specs_json TEXT,
|
||||
sats_price INTEGER NOT NULL DEFAULT 0 CHECK(sats_price >= 0),
|
||||
cents_price INTEGER NOT NULL DEFAULT 0 CHECK(cents_price >= 0),
|
||||
digital BOOLEAN NOT NULL DEFAULT 0,
|
||||
subscription BOOLEAN NOT NULL DEFAULT 0,
|
||||
image_url_0 TEXT,
|
||||
image_url_1 TEXT,
|
||||
image_url_2 TEXT,
|
||||
image_url_3 TEXT,
|
||||
image_url_4 TEXT,
|
||||
image_url_5 TEXT,
|
||||
image_url_6 TEXT,
|
||||
image_url_7 TEXT,
|
||||
image_url_8 TEXT,
|
||||
image_url_9 TEXT,
|
||||
image_url_10 TEXT,
|
||||
image_url_11 TEXT
|
||||
)");
|
||||
}
|
||||
|
||||
public static function add($title, $desc, $stock_qty, $specs_json, $sats_price, $cents_price, $digital, $subscription, $images)
|
||||
{
|
||||
$stmt = app::$db->prepare("INSERT INTO products (
|
||||
title, desc, stock_qty, specs_json, sats_price, cents_price, digital, subscription,
|
||||
image_url_0, image_url_1, image_url_2, image_url_3, image_url_4, image_url_5,
|
||||
image_url_6, image_url_7, image_url_8, image_url_9, image_url_10, image_url_11
|
||||
) VALUES (
|
||||
:title, :desc, :stock_qty, :specs_json, :sats_price, :cents_price, :digital, :subscription,
|
||||
:image_url_0, :image_url_1, :image_url_2, :image_url_3, :image_url_4, :image_url_5,
|
||||
:image_url_6, :image_url_7, :image_url_8, :image_url_9, :image_url_10, :image_url_11
|
||||
)");
|
||||
|
||||
$stmt->execute([
|
||||
':title' => $title,
|
||||
':desc' => $desc,
|
||||
':stock_qty' => $stock_qty,
|
||||
':specs_json' => $specs_json,
|
||||
':sats_price' => $sats_price,
|
||||
':cents_price' => $cents_price,
|
||||
':digital' => (int) $digital,
|
||||
':subscription' => (int) $subscription,
|
||||
':image_url_0' => $images[0] ?? null,
|
||||
':image_url_1' => $images[1] ?? null,
|
||||
':image_url_2' => $images[2] ?? null,
|
||||
':image_url_3' => $images[3] ?? null,
|
||||
':image_url_4' => $images[4] ?? null,
|
||||
':image_url_5' => $images[5] ?? null,
|
||||
':image_url_6' => $images[6] ?? null,
|
||||
':image_url_7' => $images[7] ?? null,
|
||||
':image_url_8' => $images[8] ?? null,
|
||||
':image_url_9' => $images[9] ?? null,
|
||||
':image_url_10' => $images[10] ?? null,
|
||||
':image_url_11' => $images[11] ?? null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
70
src/models/quote_items.php
Normal file
70
src/models/quote_items.php
Normal file
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
namespace app\models;
|
||||
|
||||
use app\app;
|
||||
|
||||
class quote_items
|
||||
{
|
||||
public static function init()
|
||||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS quote_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
quote_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
quantity INTEGER NOT NULL CHECK(quantity > 0),
|
||||
price REAL NOT NULL CHECK(price >= 0),
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (quote_id) REFERENCES quotes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
);");
|
||||
}
|
||||
|
||||
public static function addItem(int $quoteId, int $productId, int $quantity, float $price)
|
||||
{
|
||||
if ($quantity <= 0) {
|
||||
throw new \InvalidArgumentException('Quantity must be greater than zero.');
|
||||
}
|
||||
if ($price < 0) {
|
||||
throw new \InvalidArgumentException('Price must be non-negative.');
|
||||
}
|
||||
|
||||
$stmt = app::$db->prepare("INSERT INTO quote_items (quote_id, product_id, quantity, price)
|
||||
VALUES (:quote_id, :product_id, :quantity, :price)");
|
||||
$stmt->execute([
|
||||
'quote_id' => $quoteId,
|
||||
'product_id' => $productId,
|
||||
'quantity' => $quantity,
|
||||
'price' => $price
|
||||
]);
|
||||
}
|
||||
|
||||
public static function updateItem(int $quoteItemId, int $quantity, float $price)
|
||||
{
|
||||
if ($quantity <= 0) {
|
||||
throw new \InvalidArgumentException('Quantity must be greater than zero.');
|
||||
}
|
||||
if ($price < 0) {
|
||||
throw new \InvalidArgumentException('Price must be non-negative.');
|
||||
}
|
||||
|
||||
$stmt = app::$db->prepare("UPDATE quote_items SET quantity = :quantity, price = :price WHERE quote_item_id = :quote_item_id");
|
||||
$stmt->execute([
|
||||
'quote_item_id' => $quoteItemId,
|
||||
'quantity' => $quantity,
|
||||
'price' => $price
|
||||
]);
|
||||
}
|
||||
|
||||
public static function removeItem(int $quoteItemId)
|
||||
{
|
||||
$stmt = app::$db->prepare("DELETE FROM quote_items WHERE quote_item_id = :quote_item_id");
|
||||
$stmt->execute(['quote_item_id' => $quoteItemId]);
|
||||
}
|
||||
|
||||
public static function getQuoteItems(int $quoteId): array
|
||||
{
|
||||
$stmt = app::$db->prepare("SELECT * FROM quote_items WHERE quote_id = :quote_id");
|
||||
$stmt->execute(['quote_id' => $quoteId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
}
|
69
src/models/quotes.php
Normal file
69
src/models/quotes.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
namespace app\models;
|
||||
|
||||
use app\app;
|
||||
|
||||
class quotes
|
||||
{
|
||||
private const STATUSES = [
|
||||
'DRAFT', 'PUBLISHED', 'SENT', 'PURCHASED',
|
||||
'EXPIRED', 'CANCELED'
|
||||
];
|
||||
|
||||
public static function init()
|
||||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS quotes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT CHECK(status IN ('" . implode("', '", self::STATUSES) . "')) NOT NULL DEFAULT 'DRAFT',
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);");
|
||||
}
|
||||
|
||||
public static function createQuote(int $userId, string $status = 'DRAFT'): int
|
||||
{
|
||||
self::validateStatus($status);
|
||||
|
||||
$stmt = app::$db->prepare("INSERT INTO quotes (user_id, status)
|
||||
VALUES (:user_id, :status)");
|
||||
$stmt->execute([
|
||||
'user_id' => $userId,
|
||||
'status' => $status
|
||||
]);
|
||||
|
||||
return app::$db->lastInsertId();
|
||||
}
|
||||
|
||||
public static function updateStatus(int $quoteId, string $status)
|
||||
{
|
||||
self::validateStatus($status);
|
||||
|
||||
$stmt = app::$db->prepare("UPDATE quotes SET status = :status WHERE quote_id = :quote_id");
|
||||
$stmt->execute([
|
||||
'quote_id' => $quoteId,
|
||||
'status' => $status
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getQuote(int $quoteId): array
|
||||
{
|
||||
$stmt = app::$db->prepare("SELECT * FROM quotes WHERE quote_id = :quote_id");
|
||||
$stmt->execute(['quote_id' => $quoteId]);
|
||||
return $stmt->fetch() ?: [];
|
||||
}
|
||||
|
||||
public static function getUserQuotes(int $userId): array
|
||||
{
|
||||
$stmt = app::$db->prepare("SELECT * FROM quotes WHERE user_id = :user_id ORDER BY created_at DESC");
|
||||
$stmt->execute(['user_id' => $userId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
private static function validateStatus(string $status)
|
||||
{
|
||||
if (!in_array($status, self::STATUSES, true)) {
|
||||
throw new \InvalidArgumentException("Invalid quote status: $status");
|
||||
}
|
||||
}
|
||||
}
|
101
src/models/subscriptions.php
Normal file
101
src/models/subscriptions.php
Normal file
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
namespace app\models;
|
||||
|
||||
use app\app;
|
||||
|
||||
class Subscriptions
|
||||
{
|
||||
const STATES = [
|
||||
'TRIAL', 'START', 'RENEWAL'
|
||||
];
|
||||
|
||||
const STATUS = [
|
||||
'COMPLETED', 'CANCELED'
|
||||
];
|
||||
|
||||
public static function init()
|
||||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS subscriptions (
|
||||
subscription_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
start_date DATETIME NOT NULL,
|
||||
renews_at DATETIME NOT NULL,
|
||||
status TEXT CHECK(status IN ('" . implode("', '", self::STATUS) . "')) NOT NULL DEFAULT 'COMPLETED',
|
||||
state TEXT CHECK(state IN ('" . implode("', '", self::STATES) . "')) NOT NULL DEFAULT 'TRIAL',
|
||||
invoice_date DATETIME NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
);");
|
||||
}
|
||||
|
||||
public static function createSubscription(int $userId, int $productId, string $state = 'TRIAL', string $status = 'COMPLETED', string $startDate, string $renewAt, string $invoiceDate): int
|
||||
{
|
||||
self::validateState($state);
|
||||
self::validateStatus($status);
|
||||
|
||||
$stmt = app::$db->prepare("INSERT INTO subscriptions (user_id, product_id, state, status, start_date, renews_at, invoice_date)
|
||||
VALUES (:user_id, :product_id, :state, :status, :start_date, :renews_at, :invoice_date)");
|
||||
$stmt->execute([
|
||||
'user_id' => $userId,
|
||||
'product_id' => $productId,
|
||||
'state' => $state,
|
||||
'status' => $status,
|
||||
'start_date' => $startDate,
|
||||
'renews_at' => $renewAt,
|
||||
'invoice_date' => $invoiceDate
|
||||
]);
|
||||
|
||||
return app::$db->lastInsertId();
|
||||
}
|
||||
|
||||
public static function updateState(int $subscriptionId, string $state)
|
||||
{
|
||||
self::validateState($state);
|
||||
|
||||
$stmt = app::$db->prepare("UPDATE subscriptions SET state = :state WHERE subscription_id = :subscription_id");
|
||||
$stmt->execute([
|
||||
'subscription_id' => $subscriptionId,
|
||||
'state' => $state
|
||||
]);
|
||||
}
|
||||
|
||||
public static function updateStatus(int $subscriptionId, string $status)
|
||||
{
|
||||
self::validateStatus($status);
|
||||
|
||||
$stmt = app::$db->prepare("UPDATE subscriptions SET status = :status WHERE subscription_id = :subscription_id");
|
||||
$stmt->execute([
|
||||
'subscription_id' => $subscriptionId,
|
||||
'status' => $status
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getSubscription(int $subscriptionId): array
|
||||
{
|
||||
$stmt = app::$db->prepare("SELECT * FROM subscriptions WHERE subscription_id = :subscription_id");
|
||||
$stmt->execute(['subscription_id' => $subscriptionId]);
|
||||
return $stmt->fetch() ?: [];
|
||||
}
|
||||
|
||||
public static function getUserSubscriptions(int $userId): array
|
||||
{
|
||||
$stmt = app::$db->prepare("SELECT * FROM subscriptions WHERE user_id = :user_id ORDER BY start_date DESC");
|
||||
$stmt->execute(['user_id' => $userId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
private static function validateState(string $state)
|
||||
{
|
||||
if (!in_array($state, self::STATES, true)) {
|
||||
throw new \InvalidArgumentException("Invalid subscription state: $state");
|
||||
}
|
||||
}
|
||||
|
||||
private static function validateStatus(string $status)
|
||||
{
|
||||
if (!in_array($status, self::STATUS, true)) {
|
||||
throw new \InvalidArgumentException("Invalid subscription status: $status");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,53 +2,89 @@
|
|||
namespace app\models;
|
||||
|
||||
use app\app;
|
||||
|
||||
class transactions
|
||||
{
|
||||
const TYPES = ['CREDIT', 'REWARD', 'REDEEM', 'REVOKE', 'DEPOSIT'];
|
||||
|
||||
public static function init()
|
||||
{
|
||||
app::$db->exec("CREATE TABLE transactions (
|
||||
$typesList = "'" . implode("', '", self::TYPES) . "'";
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
type TEXT CHECK(transaction_type IN ('credit', 'spend', 'withdraw')) NOT NULL,
|
||||
cents REAL DEFAULT 0,
|
||||
sats REAL DEFAULT 0,
|
||||
type TEXT CHECK(type IN ($typesList)) NOT NULL,
|
||||
cents INTEGER DEFAULT 0,
|
||||
sats INTEGER DEFAULT 0,
|
||||
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)");
|
||||
}
|
||||
|
||||
public static function add($user_id, $transaction_type, $cents, $sats_amount)
|
||||
public static function add($user_id, $transaction_type, $cents, $sats)
|
||||
{
|
||||
$query = "INSERT INTO transactions (
|
||||
user_id,
|
||||
type,
|
||||
cents,
|
||||
sats
|
||||
) VALUES (
|
||||
:user_id,
|
||||
:transaction_type,
|
||||
:cents,
|
||||
:sats
|
||||
)";
|
||||
if (!in_array($transaction_type, self::TYPES)) {
|
||||
throw new \Exception("Invalid transaction type.");
|
||||
}
|
||||
if ($cents < 0 || $sats < 0) {
|
||||
throw new \Exception("Amounts must be non-negative integers.");
|
||||
}
|
||||
|
||||
$currentBalance = self::getUserBalance($user_id);
|
||||
|
||||
if (in_array($transaction_type, ['REDEEM', 'REVOKE']) && ($currentBalance['total_cents'] < $cents || $currentBalance['total_sats'] < $sats)) {
|
||||
throw new \Exception("Insufficient funds.");
|
||||
}
|
||||
|
||||
$query = "INSERT INTO transactions (user_id, type, cents, sats) VALUES (:user_id, :transaction_type, :cents, :sats)";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':user_id', $user_id);
|
||||
$stmt->bindParam(':transaction_type', $transaction_type);
|
||||
$stmt->bindParam(':cents', $cents);
|
||||
$stmt->bindParam(':sats', $sats_amount);
|
||||
$stmt->bindParam(':sats', $sats);
|
||||
$stmt->execute();
|
||||
|
||||
return app::$db->lastInsertId();
|
||||
}
|
||||
|
||||
public static function getUserBalance($user_id)
|
||||
{
|
||||
$query = "SELECT SUM(cents) AS total_cents,
|
||||
SUM(sats) AS total_sats
|
||||
FROM transactions
|
||||
WHERE user_id = :user_id";
|
||||
$query = "SELECT COALESCE(SUM(cents), 0) AS total_cents, COALESCE(SUM(sats), 0) AS total_sats FROM transactions WHERE user_id = :user_id";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':user_id', $user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetch();
|
||||
return $result;
|
||||
return $stmt->fetch();
|
||||
}
|
||||
|
||||
public static function getRecent($n)
|
||||
{
|
||||
$query = "SELECT * FROM transactions ORDER BY date DESC LIMIT :n";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':n', $n, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public static function getWhales($n, $currency)
|
||||
{
|
||||
if (!in_array($currency, ['cents', 'sats'])) {
|
||||
throw new \Exception("Invalid currency type.");
|
||||
}
|
||||
$query = "SELECT user_id, COALESCE(SUM($currency), 0) AS total FROM transactions GROUP BY user_id ORDER BY total DESC LIMIT :n";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':n', $n, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public static function liabilities($currency)
|
||||
{
|
||||
if (!in_array($currency, ['cents', 'sats'])) {
|
||||
throw new \Exception("Invalid currency type.");
|
||||
}
|
||||
$query = "SELECT COALESCE(SUM($currency), 0) AS total FROM transactions";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchColumn();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,19 +18,27 @@ class user_addresses
|
|||
|
||||
public static function getShippingByUserId($id)
|
||||
{
|
||||
$addrs = app::$db->query("SELECT a.* FROM users u
|
||||
$query = "SELECT a.* FROM users u
|
||||
JOIN user_addresses ua ON u.id = ua.user_id
|
||||
JOIN addresses a ON ua.address_id = a.id
|
||||
WHERE u.id = '$id' AND a.shipping = 1")->fetch(\PDO::FETCH_ASSOC);
|
||||
WHERE u.id = :id AND a.shipping = 1";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':id', $id);
|
||||
$stmt->execute();
|
||||
$addrs = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
return [$addrs];
|
||||
}
|
||||
|
||||
public static function getBillingByUserId($id)
|
||||
{
|
||||
$addrs = app::$db->query("SELECT a.* FROM users u
|
||||
$query = "SELECT a.* FROM users u
|
||||
JOIN user_addresses ua ON u.id = ua.user_id
|
||||
JOIN addresses a ON ua.address_id = a.id
|
||||
WHERE u.id = '$id' AND a.billing = 1")->fetch(\PDO::FETCH_ASSOC);
|
||||
WHERE u.id = :id AND a.billing = 1";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':id', $id);
|
||||
$stmt->execute();
|
||||
$addrs = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
return [$addrs];
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
namespace app\models;
|
||||
|
||||
use app\app;
|
||||
use swentel\nostr\Key\Key;
|
||||
|
||||
class users
|
||||
{
|
||||
public static function init()
|
||||
|
@ -14,15 +16,44 @@ class users
|
|||
opt_in_promotional BOOLEAN NOT NULL,
|
||||
verified BOOLEAN NOT NULL,
|
||||
dark_theme BOOLEAN NOT NULL,
|
||||
generated_base58 TEXT UNIQUE,
|
||||
nsec TEXT,
|
||||
npub TEXT NOT NULL,
|
||||
attached_lightning_address TEXT,
|
||||
replace_email_token TEXT,
|
||||
name TEXT,
|
||||
company_name TEXT,
|
||||
company_type TEXT,
|
||||
company_size TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)");
|
||||
app::$db->exec('CREATE INDEX IF NOT EXISTS idx_user_email ON users (email)');
|
||||
}
|
||||
|
||||
public static function updateReplaceEmailTokenById($user_id, $replace_token)
|
||||
{
|
||||
$query = "UPDATE users SET replace_email_token = :replace_token WHERE id = :user_id";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':replace_token', $replace_token);
|
||||
$stmt->bindParam(':user_id', $user_id);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public static function updateEmailById($user_id, $email)
|
||||
{
|
||||
$query = "UPDATE users SET email = :email WHERE id = :user_id";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':email', $email);
|
||||
$stmt->bindParam(':user_id', $user_id);
|
||||
$stmt->execute();
|
||||
users::updateReplaceEmailTokenById($user_id, null);
|
||||
}
|
||||
|
||||
public static function getByReplaceEmailToken($token)
|
||||
{
|
||||
$query = "SELECT * FROM users WHERE replace_email_token = :token";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':token', $token);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public static function updateProfileById($user_id, $post)
|
||||
|
@ -44,20 +75,29 @@ class users
|
|||
|
||||
public static function add($email, $ship_id, $bill_id, $opt_in_promotional, $verified, $dark_theme)
|
||||
{
|
||||
$key = new Key();
|
||||
$private_key = $key->generatePrivateKey();
|
||||
$public_key = $key->getPublicKey($private_key);
|
||||
$npub = $key->convertPublicKeyToBech32($public_key);
|
||||
$nsec = $key->convertPrivateKeyToBech32($private_key);
|
||||
$query = "INSERT INTO users (
|
||||
email,
|
||||
shipping_address_id,
|
||||
billing_address_id,
|
||||
opt_in_promotional,
|
||||
verified,
|
||||
dark_theme
|
||||
dark_theme,
|
||||
nsec,
|
||||
npub
|
||||
) VALUES (
|
||||
:email,
|
||||
:shipping_address_id,
|
||||
:billing_address_id,
|
||||
:opt_in_promotional,
|
||||
:verified,
|
||||
:dark_theme
|
||||
:dark_theme,
|
||||
:nsec,
|
||||
:npub
|
||||
)";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':email', $email);
|
||||
|
@ -66,18 +106,35 @@ class users
|
|||
$stmt->bindParam(':opt_in_promotional', $opt_in_promotional);
|
||||
$stmt->bindParam(':verified', $verified);
|
||||
$stmt->bindParam(':dark_theme', $dark_theme);
|
||||
$stmt->bindParam(':nsec', $nsec);
|
||||
$stmt->bindParam(':npub', $npub);
|
||||
$stmt->execute();
|
||||
return app::$db->lastInsertId();
|
||||
|
||||
}
|
||||
|
||||
public static function verify($email)
|
||||
{
|
||||
app::$db->exec("UPDATE users SET verified = 1 WHERE email = '$email'");
|
||||
$query = "UPDATE users SET verified = 1 WHERE email = :email";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':email', $email);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public static function getById($id)
|
||||
{
|
||||
$query = "SELECT * FROM users WHERE id = :id";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':id', $id);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public static function getByEmail($email)
|
||||
{
|
||||
return app::$db->query("SELECT * FROM users WHERE email = '$email'")->fetch(\PDO::FETCH_ASSOC);
|
||||
$query = "SELECT * FROM users WHERE email = :email";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':email', $email);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/* #Mega Menu Styles
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.mega-menu {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
z-index: -900;
|
||||
left: 0;
|
||||
top: 38px;
|
||||
position: absolute;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
transition: all 0.15s linear 0s;
|
||||
}
|
||||
/* #hoverable Class Styles */
|
||||
.hoverable {
|
||||
position: static;
|
||||
}
|
||||
.hoverable > a:after {
|
||||
content: "\25BC";
|
||||
font-size: 10px;
|
||||
padding-left: 6px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
.hoverable:hover .mega-menu {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
z-index: 900;
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
<div class='flex flex-col'>
|
||||
<span>{{ default_billing.name }}</span>
|
||||
<span>{{ default_billing.company }}</span>
|
||||
<span>{{ default_billing.street }}</span>
|
||||
<span>{{ default_billing.boxapt }}</span>
|
||||
<span>{{ default_billing.addressLine1 }}</span>
|
||||
<span>{{ default_billing.addressLine2 }}</span>
|
||||
<span>{{ default_billing.city }}, {{ default_billing.state }} {{ default_billing.zip }}</span>
|
||||
<span>{{ default_billing.phone }}</span>
|
||||
</div>
|
||||
|
|
|
@ -40,8 +40,8 @@
|
|||
<div class="flex flex-col gap-1">
|
||||
<h4 class="font-semibold">{{ default_shipping.name }}</h4>
|
||||
<h4 class="font-semibold">{{ default_shipping.company }}</h4>
|
||||
<h4 class="font-semibold">{{ default_shipping.street }}</h4>
|
||||
<h4 class="font-semibold">{{ default_billing.boxapt }}</h4>
|
||||
<h4 class="font-semibold">{{ default_shipping.addressLine1 }}</h4>
|
||||
<h4 class="font-semibold">{{ default_billing.addressLine2 }}</h4>
|
||||
<h4 class="font-semibold">{{ default_shipping.city }}, {{ default_shipping.state }}, {{ default_shipping.zip }}</h4>
|
||||
<h4 class="font-semibold">{{ default_shipping.phone }}</h4>
|
||||
</div>
|
||||
|
@ -55,8 +55,8 @@
|
|||
<div class="flex flex-col gap-1">
|
||||
<h4 class="font-semibold">{{ default_billing.name }}</h4>
|
||||
<h4 class="font-semibold">{{ default_billing.company }}</h4>
|
||||
<h4 class="font-semibold">{{ default_billing.street }}</h4>
|
||||
<h4 class="font-semibold">{{ default_billing.boxapt }}</h4>
|
||||
<h4 class="font-semibold">{{ default_billing.addressLine1 }}</h4>
|
||||
<h4 class="font-semibold">{{ default_billing.addressLine2 }}</h4>
|
||||
<h4 class="font-semibold">{{ default_billing.city }}, {{ default_billing.state }}, {{ default_billing.zip }}</h4>
|
||||
<h4 class="font-semibold">{{ default_billing.phone }}</h4>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% include 'lib/rule.twig' %}
|
||||
</div>
|
||||
{% include 'lib/alert.twig' %}
|
||||
<form action="/magic-link" method="get" class="flex flex-col gap-4">
|
||||
<form action="/account/login" method="post" class="flex flex-col gap-4">
|
||||
{% include 'lib/input.twig' with {
|
||||
type: 'email',
|
||||
name: 'email',
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
<div class='flex flex-col'>
|
||||
<span>{{ default_shipping.name }}</span>
|
||||
<span>{{ default_shipping.company }}</span>
|
||||
<span>{{ default_shipping.street }}</span>
|
||||
<span>{{ default_shipping.boxapt }}</span>
|
||||
<span>{{ default_shipping.addressLine1 }}</span>
|
||||
<span>{{ default_shipping.addressLine2 }}</span>
|
||||
<span>{{ default_shipping.city }}, {{ default_shipping.state }} {{ default_shipping.zip }}</span>
|
||||
<span>{{ default_shipping.phone }}</span>
|
||||
</div>
|
||||
|
|
|
@ -34,9 +34,9 @@
|
|||
{% include 'lib/form/address.twig' with {
|
||||
action: 'shipping',
|
||||
name: session.last_post.shipping_name,
|
||||
street: session.last_post.shipping_street,
|
||||
addressLine1: session.last_post.shipping_addressLine1,
|
||||
company: session.last_post.shipping_company,
|
||||
boxapt: session.last_post.shipping_boxapt,
|
||||
addressLine2: session.last_post.shipping_addressLine2,
|
||||
city: session.last_post.shipping_city,
|
||||
state: session.last_post.shipping_state,
|
||||
zip: session.last_post.shipping_zip,
|
||||
|
@ -51,16 +51,17 @@
|
|||
</h4>
|
||||
{% include 'lib/toggle.twig' with {
|
||||
label: 'Same as shipping',
|
||||
name: 'use_shipping'
|
||||
name: 'use_shipping',
|
||||
on: true
|
||||
} %}
|
||||
</div>
|
||||
<div id="billing-address" style="display: none;">
|
||||
{% include 'lib/form/address.twig' with {
|
||||
action: 'billing',
|
||||
name: session.last_post.billing_name,
|
||||
street: session.last_post.billing_street,
|
||||
addressLine1: session.last_post.billing_addressLine1,
|
||||
company: session.last_post.billing_company,
|
||||
boxapt: session.last_post.billing_boxapt,
|
||||
addressLine2: session.last_post.billing_addressLine2,
|
||||
city: session.last_post.billing_city,
|
||||
state: session.last_post.billing_state,
|
||||
zip: session.last_post.billing_zip,
|
||||
|
|
|
@ -1,3 +1,34 @@
|
|||
<style>
|
||||
/* #Mega Menu Styles
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.mega-menu {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
z-index: -900;
|
||||
left: 0;
|
||||
top: 38px;
|
||||
position: absolute;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
transition: all 0.15s linear 0s;
|
||||
}
|
||||
/* #hoverable Class Styles */
|
||||
.hoverable {
|
||||
position: static;
|
||||
}
|
||||
.hoverable > a:after {
|
||||
content: "\25BC";
|
||||
font-size: 10px;
|
||||
padding-left: 6px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
.hoverable:hover .mega-menu {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
z-index: 900;
|
||||
}
|
||||
</style>
|
||||
<header class="flex flex-col items-center w-full gap-3 mb-8">
|
||||
<div class="{{ colors.header.banner }} py-1 text-sm flex w-full justify-center">
|
||||
<div class="w-[97%] lg:w-[90%] xl:w-4/5 flex justify-between">
|
||||
|
|
|
@ -14,16 +14,16 @@
|
|||
} %}
|
||||
{% include 'lib/input.twig' with {
|
||||
type: 'text',
|
||||
name: action ~ '_street',
|
||||
label: 'Street',
|
||||
value: street
|
||||
name: action ~ '_addressLine1',
|
||||
label: 'Address Line 1',
|
||||
value: addressLine1
|
||||
} %}
|
||||
{% include 'lib/input.twig' with {
|
||||
type: 'text',
|
||||
name: action ~ '_boxapt',
|
||||
label: 'PO Box/Apt#',
|
||||
name: action ~ '_addressLine2',
|
||||
label: 'Address Line 2',
|
||||
optional: true,
|
||||
value: boxapt
|
||||
value: addressLine2
|
||||
} %}
|
||||
<div class="flex gap-4">
|
||||
{% include 'lib/input.twig' with {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue