This commit is contained in:
root
2026-01-03 06:08:17 +00:00
parent 7c609ec40b
commit 92b4100cbf
5 changed files with 352 additions and 4 deletions

23
.env.local Normal file
View File

@@ -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

View File

@@ -3,7 +3,7 @@
-- https://www.phpmyadmin.net/ -- https://www.phpmyadmin.net/
-- --
-- Host: localhost:3306 -- 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 -- Server version: 10.11.14-MariaDB-0+deb12u2
-- PHP Version: 8.2.29 -- PHP Version: 8.2.29
@@ -32,6 +32,7 @@ CREATE TABLE `buyers` (
`username` varchar(255) NOT NULL, `username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL, `password` varchar(255) NOT NULL,
`email` 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() `created_at` datetime NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) 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` -- Table structure for table `pending_orders`
-- --
@@ -96,12 +123,44 @@ CREATE TABLE `pending_orders` (
`size` int(11) NOT NULL, `size` int(11) NOT NULL,
`price_amount` decimal(10,2) NOT NULL, `price_amount` decimal(10,2) NOT NULL,
`price_currency` varchar(10) NOT NULL DEFAULT 'chf', `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(), `created_at` datetime NOT NULL DEFAULT current_timestamp(),
`expires_at` datetime NOT NULL DEFAULT (current_timestamp() + interval 10 minute) `expires_at` datetime NOT NULL DEFAULT (current_timestamp() + interval 10 minute)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) 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` -- Table structure for table `referrals`
-- --
@@ -125,6 +184,9 @@ CREATE TABLE `sales` (
`buyer_data_id` int(11) NOT NULL, `buyer_data_id` int(11) NOT NULL,
`size` int(11) NOT NULL DEFAULT 1, `size` int(11) NOT NULL DEFAULT 1,
`payment_id` text NOT NULL DEFAULT '', `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() `created_at` datetime NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
@@ -142,7 +204,8 @@ ALTER TABLE `buyers`
-- Indexes for table `buyer_data` -- Indexes for table `buyer_data`
-- --
ALTER 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` -- Indexes for table `deliveries`
@@ -157,6 +220,21 @@ ALTER TABLE `deliveries`
ALTER TABLE `drops` ALTER TABLE `drops`
ADD PRIMARY KEY (`id`); 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` -- Indexes for table `pending_orders`
-- --
@@ -169,6 +247,22 @@ ALTER TABLE `pending_orders`
ADD KEY `idx_expires_at` (`expires_at`), ADD KEY `idx_expires_at` (`expires_at`),
ADD KEY `buyer_data_id` (`buyer_data_id`); 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` -- Indexes for table `referrals`
-- --
@@ -214,12 +308,30 @@ ALTER TABLE `deliveries`
ALTER TABLE `drops` ALTER TABLE `drops`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; 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` -- AUTO_INCREMENT for table `pending_orders`
-- --
ALTER TABLE `pending_orders` ALTER TABLE `pending_orders`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; 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` -- AUTO_INCREMENT for table `referrals`
-- --
@@ -236,12 +348,30 @@ ALTER TABLE `sales`
-- Constraints for dumped tables -- 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` -- Constraints for table `deliveries`
-- --
ALTER TABLE `deliveries` ALTER TABLE `deliveries`
ADD CONSTRAINT `deliveries_ibfk_1` FOREIGN KEY (`sale_id`) REFERENCES `sales` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 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` -- 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_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`); 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` -- 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_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_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`); 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; COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;

View File

@@ -364,14 +364,18 @@ export async function movePaymentToSalesWithInventoryCheck(
} }
// Create sale record (Step 5 from guide) // Create sale record (Step 5 from guide)
// Include price_amount, price_currency, and points_used from pending order
const [insertResult] = await connection.execute( 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.drop_id,
pendingOrder.buyer_id, pendingOrder.buyer_id,
pendingOrder.buyer_data_id, pendingOrder.buyer_data_id,
pendingOrder.size, 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<BuyerDataIn
throw error; throw error;
} }
} }
/**
* Award referral points to the referrer when a sale is completed
* Calls the stored procedure award_referral_points
*/
export async function awardReferralPoints(saleId: number): Promise<void> {
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}`);
}
}
}

View File

@@ -12,6 +12,7 @@ export interface PendingOrder {
size: number; size: number;
price_amount: number; price_amount: number;
price_currency: string; price_currency: string;
points_used: number;
created_at: Date; created_at: Date;
expires_at: Date; expires_at: Date;
} }
@@ -23,6 +24,9 @@ export interface Sale {
buyer_data_id: number; buyer_data_id: number;
size: number; size: number;
payment_id: string; payment_id: string;
price_amount: number | null;
price_currency: string;
points_used: number;
created_at: Date; created_at: Date;
} }

View File

@@ -8,6 +8,7 @@ import {
movePaymentToSalesWithInventoryCheck, movePaymentToSalesWithInventoryCheck,
paymentExistsInSales, paymentExistsInSales,
deletePendingOrderById, deletePendingOrderById,
awardReferralPoints,
} from '../database/paymentService'; } from '../database/paymentService';
const router = Router(); const router = Router();
@@ -171,6 +172,18 @@ async function handleSuccessfulPayment(pendingOrder: any, paymentId: string): Pr
const sale = await movePaymentToSalesWithInventoryCheck(pendingOrder.id, paymentId); const sale = await movePaymentToSalesWithInventoryCheck(pendingOrder.id, paymentId);
console.log(`✅ Successfully processed payment ${paymentId}. Sale ID: ${sale.id}`); 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) { } catch (error) {
// Error handling: if inventory check fails, the pending order is already deleted in the transaction // Error handling: if inventory check fails, the pending order is already deleted in the transaction
if (error instanceof Error) { if (error instanceof Error) {