From 92b4100cbf120c53258340210e866859bff67ab6 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 3 Jan 2026 06:08:17 +0000 Subject: [PATCH] final --- .env.local | 23 +++ cbd420(1).sql | 291 ++++++++++++++++++++++++++++++++- src/database/paymentService.ts | 25 ++- src/database/types.ts | 4 + src/routes/ipn.ts | 13 ++ 5 files changed, 352 insertions(+), 4 deletions(-) create mode 100644 .env.local diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..76c9b09 --- /dev/null +++ b/.env.local @@ -0,0 +1,23 @@ +# NOWPayments IPN Secret Key +# Get this from your NOWPayments dashboard -> Store Settings -> IPN Secret Key +NOWPAYMENTS_IPN_SECRET_KEY=PpzD3PupiX7CdXN5+ZAs5xYFe0zBFh// + +# Server Port (optional, defaults to 3000) +PORT=3421 + +# Node Environment +NODE_ENV=development + +# Database Configuration +DB_HOST=localhost +DB_PORT=3306 +DB_USER=cbd420 +DB_PASSWORD=76HkE-mQ1HeH-PLk +DB_NAME=cbd420 + +SMTP_HOST=mail.playpoolstudios.com +SMTP_PORT=465 +SMTP_USER=sewmina@playpoolstudios.com +SMTP_PASSWORD=TcSp419603 +SMTP_FROM_EMAIL=sewmina@playpoolstudios.com +SMTP_FROM_NAME=CBD420 diff --git a/cbd420(1).sql b/cbd420(1).sql index 229bd16..7729bd5 100644 --- a/cbd420(1).sql +++ b/cbd420(1).sql @@ -3,7 +3,7 @@ -- https://www.phpmyadmin.net/ -- -- Host: localhost:3306 --- Generation Time: Dec 21, 2025 at 09:44 AM +-- Generation Time: Dec 28, 2025 at 01:36 AM -- Server version: 10.11.14-MariaDB-0+deb12u2 -- PHP Version: 8.2.29 @@ -32,6 +32,7 @@ CREATE TABLE `buyers` ( `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, + `referral_points` decimal(10,2) NOT NULL DEFAULT 0.00, `created_at` datetime NOT NULL DEFAULT current_timestamp() ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; @@ -82,6 +83,32 @@ CREATE TABLE `drops` ( -- -------------------------------------------------------- +-- +-- Table structure for table `drop_images` +-- + +CREATE TABLE `drop_images` ( + `id` int(11) NOT NULL, + `drop_id` int(11) NOT NULL, + `image_url` varchar(255) NOT NULL, + `display_order` int(11) NOT NULL DEFAULT 0, + `created_at` datetime NOT NULL DEFAULT current_timestamp() +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `notification_subscribers` +-- + +CREATE TABLE `notification_subscribers` ( + `address` varchar(100) NOT NULL, + `type` text NOT NULL DEFAULT '\'email\'', + `buyer_id` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- -------------------------------------------------------- + -- -- Table structure for table `pending_orders` -- @@ -96,12 +123,44 @@ CREATE TABLE `pending_orders` ( `size` int(11) NOT NULL, `price_amount` decimal(10,2) NOT NULL, `price_currency` varchar(10) NOT NULL DEFAULT 'chf', + `points_used` decimal(10,2) NOT NULL DEFAULT 0.00, `created_at` datetime NOT NULL DEFAULT current_timestamp(), `expires_at` datetime NOT NULL DEFAULT (current_timestamp() + interval 10 minute) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -- -------------------------------------------------------- +-- +-- Table structure for table `referral_point_transactions` +-- + +CREATE TABLE `referral_point_transactions` ( + `id` int(11) NOT NULL, + `buyer_id` int(11) NOT NULL, + `points` decimal(10,2) NOT NULL, + `type` enum('earned','spent') NOT NULL, + `sale_id` int(11) DEFAULT NULL, + `pending_order_id` int(11) DEFAULT NULL, + `description` text DEFAULT NULL, + `created_at` datetime NOT NULL DEFAULT current_timestamp() +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `referral_settings` +-- + +CREATE TABLE `referral_settings` ( + `id` int(11) NOT NULL, + `setting_key` varchar(100) NOT NULL, + `setting_value` varchar(255) NOT NULL, + `description` text DEFAULT NULL, + `updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- -------------------------------------------------------- + -- -- Table structure for table `referrals` -- @@ -125,6 +184,9 @@ CREATE TABLE `sales` ( `buyer_data_id` int(11) NOT NULL, `size` int(11) NOT NULL DEFAULT 1, `payment_id` text NOT NULL DEFAULT '', + `price_amount` decimal(10,2) DEFAULT NULL, + `price_currency` varchar(10) NOT NULL DEFAULT 'chf', + `points_used` decimal(10,2) NOT NULL DEFAULT 0.00, `created_at` datetime NOT NULL DEFAULT current_timestamp() ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; @@ -142,7 +204,8 @@ ALTER TABLE `buyers` -- Indexes for table `buyer_data` -- ALTER TABLE `buyer_data` - ADD PRIMARY KEY (`id`); + ADD PRIMARY KEY (`id`), + ADD KEY `buyer_id` (`buyer_id`); -- -- Indexes for table `deliveries` @@ -157,6 +220,21 @@ ALTER TABLE `deliveries` ALTER TABLE `drops` ADD PRIMARY KEY (`id`); +-- +-- Indexes for table `drop_images` +-- +ALTER TABLE `drop_images` + ADD PRIMARY KEY (`id`), + ADD KEY `drop_id` (`drop_id`), + ADD KEY `idx_drop_images_drop_order` (`drop_id`,`display_order`); + +-- +-- Indexes for table `notification_subscribers` +-- +ALTER TABLE `notification_subscribers` + ADD PRIMARY KEY (`address`), + ADD KEY `buyer_id` (`buyer_id`); + -- -- Indexes for table `pending_orders` -- @@ -169,6 +247,22 @@ ALTER TABLE `pending_orders` ADD KEY `idx_expires_at` (`expires_at`), ADD KEY `buyer_data_id` (`buyer_data_id`); +-- +-- Indexes for table `referral_point_transactions` +-- +ALTER TABLE `referral_point_transactions` + ADD PRIMARY KEY (`id`), + ADD KEY `buyer_id` (`buyer_id`), + ADD KEY `sale_id` (`sale_id`), + ADD KEY `pending_order_id` (`pending_order_id`); + +-- +-- Indexes for table `referral_settings` +-- +ALTER TABLE `referral_settings` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `setting_key` (`setting_key`); + -- -- Indexes for table `referrals` -- @@ -214,12 +308,30 @@ ALTER TABLE `deliveries` ALTER TABLE `drops` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; +-- +-- AUTO_INCREMENT for table `drop_images` +-- +ALTER TABLE `drop_images` + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; + -- -- AUTO_INCREMENT for table `pending_orders` -- ALTER TABLE `pending_orders` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; +-- +-- AUTO_INCREMENT for table `referral_point_transactions` +-- +ALTER TABLE `referral_point_transactions` + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `referral_settings` +-- +ALTER TABLE `referral_settings` + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; + -- -- AUTO_INCREMENT for table `referrals` -- @@ -236,12 +348,30 @@ ALTER TABLE `sales` -- Constraints for dumped tables -- +-- +-- Constraints for table `buyer_data` +-- +ALTER TABLE `buyer_data` + ADD CONSTRAINT `buyer_data_ibfk_1` FOREIGN KEY (`buyer_id`) REFERENCES `buyers` (`id`); + -- -- Constraints for table `deliveries` -- ALTER TABLE `deliveries` ADD CONSTRAINT `deliveries_ibfk_1` FOREIGN KEY (`sale_id`) REFERENCES `sales` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; +-- +-- Constraints for table `drop_images` +-- +ALTER TABLE `drop_images` + ADD CONSTRAINT `drop_images_ibfk_1` FOREIGN KEY (`drop_id`) REFERENCES `drops` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- +-- Constraints for table `notification_subscribers` +-- +ALTER TABLE `notification_subscribers` + ADD CONSTRAINT `notification_subscribers_ibfk_1` FOREIGN KEY (`buyer_id`) REFERENCES `buyers` (`id`); + -- -- Constraints for table `pending_orders` -- @@ -250,6 +380,14 @@ ALTER TABLE `pending_orders` ADD CONSTRAINT `pending_orders_ibfk_2` FOREIGN KEY (`buyer_id`) REFERENCES `buyers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, ADD CONSTRAINT `pending_orders_ibfk_3` FOREIGN KEY (`buyer_data_id`) REFERENCES `buyer_data` (`id`); +-- +-- Constraints for table `referral_point_transactions` +-- +ALTER TABLE `referral_point_transactions` + ADD CONSTRAINT `referral_point_transactions_ibfk_1` FOREIGN KEY (`buyer_id`) REFERENCES `buyers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT `referral_point_transactions_ibfk_2` FOREIGN KEY (`sale_id`) REFERENCES `sales` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + ADD CONSTRAINT `referral_point_transactions_ibfk_3` FOREIGN KEY (`pending_order_id`) REFERENCES `pending_orders` (`id`) ON DELETE SET NULL ON UPDATE CASCADE; + -- -- Constraints for table `referrals` -- @@ -264,6 +402,155 @@ ALTER TABLE `sales` ADD CONSTRAINT `sales_ibfk_1` FOREIGN KEY (`drop_id`) REFERENCES `drops` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, ADD CONSTRAINT `sales_ibfk_2` FOREIGN KEY (`buyer_id`) REFERENCES `buyers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, ADD CONSTRAINT `sales_ibfk_3` FOREIGN KEY (`buyer_data_id`) REFERENCES `buyer_data` (`id`); + +-- +-- Insert default referral settings +-- +INSERT INTO `referral_settings` (`setting_key`, `setting_value`, `description`) VALUES +('points_per_chf', '10', 'Number of referral points earned per 1 CHF purchase by referred user'), +('points_to_chf', '100', 'Number of referral points required to redeem 1 CHF discount'); + +-- +-- Stored procedure to award referral points when a sale is completed +-- This procedure should be called after a sale is created +-- Parameters: sale_id - The ID of the sale that was just created +-- +DELIMITER $$ + +CREATE PROCEDURE `award_referral_points`(IN p_sale_id INT) +BEGIN + DECLARE v_buyer_id INT; + DECLARE v_referrer_id INT; + DECLARE v_price_amount DECIMAL(10,2); + DECLARE v_points_per_chf DECIMAL(10,2); + DECLARE v_points_earned DECIMAL(10,2); + DECLARE v_drop_id INT; + DECLARE v_size INT; + DECLARE v_ppu DECIMAL(10,2); + DECLARE v_currency VARCHAR(10); + + -- Get sale details + SELECT buyer_id, drop_id, size, COALESCE(price_amount, 0), price_currency + INTO v_buyer_id, v_drop_id, v_size, v_price_amount, v_currency + FROM sales + WHERE id = p_sale_id; + + -- If price_amount is not set, calculate it from drop's ppu + IF v_price_amount = 0 OR v_price_amount IS NULL THEN + SELECT ppu INTO v_ppu FROM drops WHERE id = v_drop_id; + SET v_price_amount = v_ppu * v_size; + END IF; + + -- Get the referrer for this buyer (if any) + SELECT referrer INTO v_referrer_id + FROM referrals + WHERE referree = v_buyer_id + LIMIT 1; + + -- If there's a referrer, award points + IF v_referrer_id IS NOT NULL THEN + -- Get points_per_chf setting + SELECT CAST(setting_value AS DECIMAL(10,2)) INTO v_points_per_chf + FROM referral_settings + WHERE setting_key = 'points_per_chf' + LIMIT 1; + + -- Default to 10 if setting not found + IF v_points_per_chf IS NULL THEN + SET v_points_per_chf = 10; + END IF; + + -- Calculate points earned (based on actual purchase amount in CHF) + -- Note: This assumes price_amount is already in CHF, or convert if needed + SET v_points_earned = v_price_amount * v_points_per_chf; + + -- Update referrer's points balance + UPDATE buyers + SET referral_points = referral_points + v_points_earned + WHERE id = v_referrer_id; + + -- Record the transaction + INSERT INTO referral_point_transactions ( + buyer_id, + points, + type, + sale_id, + description + ) VALUES ( + v_referrer_id, + v_points_earned, + 'earned', + p_sale_id, + CONCAT('Points earned from referral purchase (Sale #', p_sale_id, ', Amount: ', v_price_amount, ' ', v_currency, ')') + ); + END IF; +END$$ + +-- +-- Stored procedure to spend referral points for a purchase +-- This procedure deducts points from buyer's balance and records the transaction +-- Parameters: +-- p_buyer_id - The ID of the buyer spending points +-- p_points_to_spend - Amount of points to spend +-- p_pending_order_id - Optional: ID of pending order if spending for pending order +-- p_sale_id - Optional: ID of sale if spending for completed sale +-- Returns: 1 if successful, 0 if insufficient points +-- +DELIMITER $$ + +CREATE PROCEDURE `spend_referral_points`( + IN p_buyer_id INT, + IN p_points_to_spend DECIMAL(10,2), + IN p_pending_order_id INT, + IN p_sale_id INT, + OUT p_success INT +) +BEGIN + DECLARE v_current_points DECIMAL(10,2); + DECLARE v_new_balance DECIMAL(10,2); + + -- Get current points balance + SELECT referral_points INTO v_current_points + FROM buyers + WHERE id = p_buyer_id; + + -- Check if buyer has enough points + IF v_current_points IS NULL OR v_current_points < p_points_to_spend THEN + SET p_success = 0; + ELSE + -- Deduct points + SET v_new_balance = v_current_points - p_points_to_spend; + + UPDATE buyers + SET referral_points = v_new_balance + WHERE id = p_buyer_id; + + -- Record the transaction + INSERT INTO referral_point_transactions ( + buyer_id, + points, + type, + sale_id, + pending_order_id, + description + ) VALUES ( + p_buyer_id, + p_points_to_spend, + 'spent', + p_sale_id, + p_pending_order_id, + CONCAT('Points spent for purchase', + IF(p_sale_id IS NOT NULL, CONCAT(' (Sale #', p_sale_id, ')'), ''), + IF(p_pending_order_id IS NOT NULL, CONCAT(' (Pending Order #', p_pending_order_id, ')'), '') + ) + ); + + SET p_success = 1; + END IF; +END$$ + +DELIMITER ; + COMMIT; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; diff --git a/src/database/paymentService.ts b/src/database/paymentService.ts index ecb699d..e800d95 100644 --- a/src/database/paymentService.ts +++ b/src/database/paymentService.ts @@ -364,14 +364,18 @@ export async function movePaymentToSalesWithInventoryCheck( } // Create sale record (Step 5 from guide) + // Include price_amount, price_currency, and points_used from pending order const [insertResult] = await connection.execute( - 'INSERT INTO sales (drop_id, buyer_id, buyer_data_id, size, payment_id, created_at) VALUES (?, ?, ?, ?, ?, NOW())', + 'INSERT INTO sales (drop_id, buyer_id, buyer_data_id, size, payment_id, price_amount, price_currency, points_used, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())', [ pendingOrder.drop_id, pendingOrder.buyer_id, pendingOrder.buyer_data_id, pendingOrder.size, - paymentId + paymentId, + pendingOrder.price_amount, + pendingOrder.price_currency, + pendingOrder.points_used || 0 ] ); @@ -558,3 +562,20 @@ export async function getBuyerDataById(buyerDataId: number): Promise { + try { + await pool.execute('CALL award_referral_points(?)', [saleId]); + console.log(`✅ Referral points awarded for sale ${saleId}`); + } catch (error) { + // Log error but don't throw - referral points failure shouldn't break the payment flow + console.error(`❌ Error awarding referral points for sale ${saleId}:`, error); + if (error instanceof Error) { + console.error(`❌ Error details: ${error.message}`); + } + } +} diff --git a/src/database/types.ts b/src/database/types.ts index 4456462..c8c3f80 100644 --- a/src/database/types.ts +++ b/src/database/types.ts @@ -12,6 +12,7 @@ export interface PendingOrder { size: number; price_amount: number; price_currency: string; + points_used: number; created_at: Date; expires_at: Date; } @@ -23,6 +24,9 @@ export interface Sale { buyer_data_id: number; size: number; payment_id: string; + price_amount: number | null; + price_currency: string; + points_used: number; created_at: Date; } diff --git a/src/routes/ipn.ts b/src/routes/ipn.ts index 0c569fa..66b403e 100644 --- a/src/routes/ipn.ts +++ b/src/routes/ipn.ts @@ -8,6 +8,7 @@ import { movePaymentToSalesWithInventoryCheck, paymentExistsInSales, deletePendingOrderById, + awardReferralPoints, } from '../database/paymentService'; const router = Router(); @@ -171,6 +172,18 @@ async function handleSuccessfulPayment(pendingOrder: any, paymentId: string): Pr const sale = await movePaymentToSalesWithInventoryCheck(pendingOrder.id, paymentId); console.log(`✅ Successfully processed payment ${paymentId}. Sale ID: ${sale.id}`); + + // Award referral points to the referrer (if any) + // This is done asynchronously to avoid blocking the payment flow + // Errors are logged but don't affect the payment processing + setImmediate(async () => { + try { + await awardReferralPoints(sale.id); + } catch (error) { + // Error is already logged in awardReferralPoints, just log here for context + console.error(`Failed to award referral points for sale ${sale.id}`); + } + }); } catch (error) { // Error handling: if inventory check fails, the pending order is already deleted in the transaction if (error instanceof Error) {