Created
March 14, 2025 19:31
-
-
Save slicksammy/56936caaefdd29f3f9220100d0dbb5c8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import express from 'express'; | |
| import { Request, Response } from 'express'; | |
| import { balanceStore } from '../store/balanceStore'; | |
| import { modifyBalance } from '../store/balance'; // Add import for the simple balance store | |
| import { config } from '../config'; | |
| import crypto from 'crypto'; | |
| interface SoapEvent { | |
| customer_id: string; | |
| status: string; | |
| amount: number; | |
| currency: string; | |
| event_type: string; | |
| } | |
| const CHECKOUT_DEPOSIT_SUCCEEDED = 'checkout.deposit.succeeded'; | |
| const CHECKOUT_WITHDRAWAL_HOLD = 'checkout.withdrawal.hold' | |
| const CHECKOUT_WITHDRAWAL_WITHDRAWN = 'checkout.withdrawal.withdrawn' | |
| const CHECKOUT_WITHDRAWAL_RELEASE_HOLD = 'checkout.withdrawal.release_hold' | |
| interface CheckoutEvent { | |
| type: string; | |
| data: { | |
| id: string; | |
| charge: { | |
| id: string; | |
| amount_cents: number; | |
| transaction_type: string; | |
| }; | |
| customer: { | |
| id: string; | |
| }; | |
| line_items: any[]; | |
| line_items_total_amount_cents: number | null; | |
| }; | |
| } | |
| function verifySignature(payload: string, signatureHeader: string, customerId: string): boolean { | |
| const parts = signatureHeader.split(","); | |
| const timestampPart = parts.find((p) => p.startsWith("t=")); | |
| const signaturePart = parts.find((p) => p.startsWith("v1=")); | |
| if (!timestampPart || !signaturePart) { | |
| return false; | |
| } | |
| const timestamp = timestampPart.split("=")[1]; | |
| const receivedSignature = signaturePart.split("=")[1]; | |
| // Choose the right signing secret based on customer ID | |
| let signingSecret: string; | |
| if (customerId === config.customerIdFantasy) { | |
| signingSecret = config.signingSecretFantasy; | |
| console.log("Using Fantasy signing secret for customer:", customerId); | |
| } else if (customerId === config.customerIdSweps) { | |
| signingSecret = config.signingSecretSweps; | |
| console.log("Using Sweeps signing secret for customer:", customerId); | |
| } else { | |
| console.error("Unknown customer ID:", customerId); | |
| return false; // Unknown customer ID | |
| } | |
| // Recompute the signature | |
| const message = `${timestamp}.${payload}`; | |
| const expectedSignature = crypto | |
| .createHmac("sha256", signingSecret) | |
| .update(message) | |
| .digest("hex"); | |
| // Securely compare signatures to prevent timing attacks | |
| return crypto.timingSafeEqual( | |
| Buffer.from(receivedSignature, "hex"), | |
| Buffer.from(expectedSignature, "hex") | |
| ); | |
| } | |
| const router = express.Router(); | |
| router.post('/webhooks/soapEvents', async (req: Request, res: Response) => { | |
| try { | |
| // Check if the incoming event is a payment intent succeeded event | |
| const payload = JSON.stringify(req.body); // Get raw payload | |
| const signatureHeader = req.headers["soap_signature"] as string | |
| if (req.body) { | |
| const checkoutEvent = req.body as CheckoutEvent; | |
| console.log('Received event', checkoutEvent); | |
| // Extract customer ID from the event - ensure it's always defined | |
| let customerId: string = config.customerIdFantasy; // Default to fantasy ID | |
| // Get customer ID from the customer object if available | |
| if (checkoutEvent.data.customer && checkoutEvent.data.customer.id) { | |
| customerId = checkoutEvent.data.customer.id; | |
| } | |
| // If line items exist and have customer IDs (for Sweeps) | |
| else if (checkoutEvent.data.line_items && | |
| checkoutEvent.data.line_items.length > 0 && | |
| checkoutEvent.data.line_items[0].customer_id) { | |
| customerId = checkoutEvent.data.line_items[0].customer_id; | |
| } | |
| console.log('Using customer ID for verification:', customerId); | |
| // Verify the signature with the correct customer ID | |
| if(!verifySignature(payload, signatureHeader, customerId)) { | |
| console.log("signature not verified") | |
| return res.sendStatus(500); | |
| } | |
| const eventType = checkoutEvent.type; | |
| const amountInCents = checkoutEvent.data.charge.amount_cents; | |
| // Process different event types | |
| if (eventType == CHECKOUT_DEPOSIT_SUCCEEDED) { | |
| // Handle deposits - could be Sweeps or Fantasy | |
| if (checkoutEvent.data.line_items && checkoutEvent.data.line_items.length > 0) { | |
| // Process as Sweeps deposit with line items | |
| console.log('Processing Sweeps deposit with line items'); | |
| // Process each line item | |
| for (const item of checkoutEvent.data.line_items) { | |
| const itemCustomerId = item.customer_id || customerId; | |
| const itemAmountCents = item.amount_cents || amountInCents; | |
| const coinType = item.coin_type || 'USD'; | |
| const amount = itemAmountCents; // Keep in cents, no conversion | |
| console.log(`Depositing to balance for customer ${itemCustomerId}: ${amount} ${coinType} cents`); | |
| // Update both balance systems | |
| try { | |
| const updatedBalance = balanceStore.updateBalance( | |
| itemCustomerId, | |
| amount, | |
| coinType | |
| ); | |
| console.log(`Updated ${coinType} balance for customer ${itemCustomerId} in balanceStore:`, updatedBalance); | |
| const updatedSimpleBalance = modifyBalance( | |
| itemCustomerId, | |
| amount | |
| ); | |
| console.log(`Updated balance for customer ${itemCustomerId} in simple balance store: ${updatedSimpleBalance}`); | |
| } catch (err) { | |
| console.error(`Error updating balance for customer ${itemCustomerId}:`, err); | |
| } | |
| } | |
| } else { | |
| // Process as Fantasy deposit | |
| console.log('Processing Fantasy deposit'); | |
| const amount = amountInCents; // Keep in cents, no conversion | |
| try { | |
| const updatedBalance = balanceStore.updateBalance(customerId, amount, 'USD'); | |
| console.log(`Updated balance for customer ${customerId} in balanceStore:`, updatedBalance); | |
| const updatedSimpleBalance = modifyBalance(customerId, amount); | |
| console.log(`Updated balance for customer ${customerId} in simple balance store: ${updatedSimpleBalance}`); | |
| } catch (err) { | |
| console.error(`Error updating balance for customer ${customerId}:`, err); | |
| } | |
| } | |
| return res.sendStatus(200); | |
| } else if (eventType == CHECKOUT_WITHDRAWAL_HOLD) { | |
| console.log("withdrawal hold"); | |
| // For withdrawal hold, we need to check if there's enough balance | |
| const amount = amountInCents; // Keep in cents, no conversion | |
| // Process Sweeps or Fantasy withdrawal hold | |
| if (checkoutEvent.data.line_items && checkoutEvent.data.line_items.length > 0) { | |
| // Process as Sweeps withdrawal hold with line items | |
| console.log('Processing Sweeps withdrawal hold with line items'); | |
| for (const item of checkoutEvent.data.line_items) { | |
| const itemCustomerId = item.customer_id || customerId; | |
| const itemAmountCents = item.amount_cents || amountInCents; | |
| const coinType = item.coin_type || 'USD'; | |
| const itemAmount = itemAmountCents; // Keep in cents, no conversion | |
| // Check if the user has enough balance before creating a hold | |
| const currentBalance = balanceStore.getBalance(itemCustomerId); | |
| // If no balance exists or the balance is less than the requested amount, return 400 | |
| if (!currentBalance || currentBalance.amount < itemAmount) { | |
| console.log(`Insufficient balance for customer ${itemCustomerId}. Requested: ${itemAmount}, Available: ${currentBalance?.amount || 0}`); | |
| return res.status(400).json({ | |
| error: 'Insufficient balance', | |
| requested_amount: itemAmount, | |
| available_balance: currentBalance?.amount || 0 | |
| }); | |
| } | |
| // If balance is sufficient, proceed with the hold | |
| try { | |
| const updatedBalance = balanceStore.updateBalance( | |
| itemCustomerId, | |
| -1 * itemAmount, | |
| coinType | |
| ); | |
| console.log(`Created hold on ${coinType} balance for customer ${itemCustomerId}:`, updatedBalance); | |
| const updatedSimpleBalance = modifyBalance( | |
| itemCustomerId, | |
| -1 * itemAmount | |
| ); | |
| console.log(`Created hold on balance for customer ${itemCustomerId} in simple balance store: ${updatedSimpleBalance}`); | |
| } catch (err) { | |
| console.error(`Error creating hold for customer ${itemCustomerId}:`, err); | |
| return res.status(500).json({ error: 'Error creating hold' }); | |
| } | |
| } | |
| } else { | |
| // Process as Fantasy withdrawal hold | |
| console.log('Processing Fantasy withdrawal hold'); | |
| // Check if the user has enough balance before creating a hold | |
| const currentBalance = balanceStore.getBalance(customerId); | |
| // If no balance exists or the balance is less than the requested amount, return 400 | |
| if (!currentBalance || currentBalance.amount < amount) { | |
| console.log(`Insufficient balance for customer ${customerId}. Requested: ${amount}, Available: ${currentBalance?.amount || 0}`); | |
| return res.status(400).json({ | |
| error: 'Insufficient balance', | |
| requested_amount: amount, | |
| available_balance: currentBalance?.amount || 0 | |
| }); | |
| } | |
| // If balance is sufficient, proceed with the hold | |
| try { | |
| const updatedBalance = balanceStore.updateBalance(customerId, -1 * amount, 'USD'); | |
| console.log(`Created hold on balance for customer ${customerId} in balanceStore:`, updatedBalance); | |
| const updatedSimpleBalance = modifyBalance(customerId, -1 * amount); | |
| console.log(`Created hold on balance for customer ${customerId} in simple balance store: ${updatedSimpleBalance}`); | |
| } catch (err) { | |
| console.error(`Error creating hold for customer ${customerId}:`, err); | |
| return res.status(500).json({ error: 'Error creating hold' }); | |
| } | |
| } | |
| return res.sendStatus(200); | |
| } else if (eventType == CHECKOUT_WITHDRAWAL_RELEASE_HOLD) { | |
| console.log("withdrawal release hold"); | |
| // Process Sweeps or Fantasy withdrawal release hold | |
| const amount = amountInCents; // Keep in cents, no conversion | |
| if (checkoutEvent.data.line_items && checkoutEvent.data.line_items.length > 0) { | |
| // Process as Sweeps release hold with line items | |
| console.log('Processing Sweeps release hold with line items'); | |
| for (const item of checkoutEvent.data.line_items) { | |
| const itemCustomerId = item.customer_id || customerId; | |
| const itemAmountCents = item.amount_cents || amountInCents; | |
| const coinType = item.coin_type || 'USD'; | |
| const itemAmount = itemAmountCents; // Keep in cents, no conversion | |
| try { | |
| const updatedBalance = balanceStore.updateBalance( | |
| itemCustomerId, | |
| itemAmount, | |
| coinType | |
| ); | |
| console.log(`Released hold on ${coinType} balance for customer ${itemCustomerId}:`, updatedBalance); | |
| const updatedSimpleBalance = modifyBalance( | |
| itemCustomerId, | |
| itemAmount | |
| ); | |
| console.log(`Released hold on balance for customer ${itemCustomerId} in simple balance store: ${updatedSimpleBalance}`); | |
| } catch (err) { | |
| console.error(`Error releasing hold for customer ${itemCustomerId}:`, err); | |
| } | |
| } | |
| } else { | |
| // Process as Fantasy release hold | |
| console.log('Processing Fantasy release hold'); | |
| try { | |
| const updatedBalance = balanceStore.updateBalance(customerId, amount, 'USD'); | |
| console.log(`Released hold on balance for customer ${customerId} in balanceStore:`, updatedBalance); | |
| const updatedSimpleBalance = modifyBalance(customerId, amount); | |
| console.log(`Released hold on balance for customer ${customerId} in simple balance store: ${updatedSimpleBalance}`); | |
| } catch (err) { | |
| console.error(`Error releasing hold for customer ${customerId}:`, err); | |
| } | |
| } | |
| return res.sendStatus(200); | |
| } else if (eventType == CHECKOUT_WITHDRAWAL_WITHDRAWN) { | |
| console.log("withdrawal withdrawn"); | |
| // For withdrawal withdrawn, we don't need to update the balance as it was already deducted | |
| // during the hold phase. We just need to acknowledge it. | |
| // However, we might need to do some bookkeeping in a production system. | |
| return res.sendStatus(200); | |
| } else { | |
| console.log(`Unhandled event type: ${eventType}`); | |
| return res.sendStatus(400); | |
| } | |
| } else { | |
| return res.sendStatus(400) | |
| } | |
| } catch (error) { | |
| console.error('Error processing webhook:', error); | |
| res.sendStatus(500); | |
| } | |
| }); | |
| // Updated simple webhook endpoint to log incoming payload along with additional metadata. | |
| router.post('/webhooks/simple', (req: Request, res: Response) => { | |
| console.log('Received simple webhook payload:', req.body); | |
| // Log the Origin header, if present | |
| console.log('Origin:', req.headers.origin); | |
| // Log additional metadata from the request | |
| console.log('Request IP:', req.ip); | |
| console.log('Full Request Headers:', req.headers); | |
| res.sendStatus(200); | |
| }); | |
| export default router; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment