Skip to content

Instantly share code, notes, and snippets.

@slicksammy
Created March 14, 2025 19:31
Show Gist options
  • Select an option

  • Save slicksammy/56936caaefdd29f3f9220100d0dbb5c8 to your computer and use it in GitHub Desktop.

Select an option

Save slicksammy/56936caaefdd29f3f9220100d0dbb5c8 to your computer and use it in GitHub Desktop.
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