Stripe and beacon API updates

- createPaymentIntent: improved error handling
- webhook: updated payment processing
- resolve_business: minor fix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-02-16 12:52:46 -08:00
parent ec0f2dea6a
commit 084e815c6c
3 changed files with 44 additions and 12 deletions

View file

@ -84,7 +84,7 @@ function resolveSingleBusiness(uuid, major) {
var headerImageURL = "";
if (len(qBiz.HeaderImageExtension)) {
headerImageURL = "/uploads/businesses/#qBiz.ID#/header.#qBiz.HeaderImageExtension#";
headerImageURL = "/uploads/headers/#qBiz.ID#.#qBiz.HeaderImageExtension#";
}
return {

View file

@ -73,13 +73,23 @@ try {
abort;
}
// Get order's delivery fee and grant economics (if applicable)
// Get order's delivery fee, grant economics, and existing PaymentIntent
qOrder = queryExecute("
SELECT DeliveryFee, OrderTypeID, GrantID, GrantOwnerBusinessID, GrantEconomicsType, GrantEconomicsValue
SELECT DeliveryFee, OrderTypeID, GrantID, GrantOwnerBusinessID, GrantEconomicsType, GrantEconomicsValue, StripePaymentIntentID
FROM Orders
WHERE ID = :orderID
", { orderID: orderID }, { datasource: "payfrit" });
// Check if order already has a PaymentIntent (idempotency check)
if (qOrder.recordCount > 0 && len(trim(qOrder.StripePaymentIntentID ?: "")) > 0) {
response["OK"] = false;
response["ERROR"] = "existing_payment_intent";
response["MESSAGE"] = "Order already has a PaymentIntent. Use existing checkout session or retry payment.";
response["PAYMENT_INTENT_ID"] = qOrder.StripePaymentIntentID;
writeOutput(serializeJSON(response));
abort;
}
deliveryFee = 0;
if (qOrder.recordCount > 0 && qOrder.OrderTypeID == 3) {
deliveryFee = val(qOrder.DeliveryFee);
@ -110,15 +120,18 @@ try {
// ============================================================
// Customer fee from database (PayfritFee), default 5% if not set
customerFeePercent = isNumeric(qBusiness.PayfritFee) && qBusiness.PayfritFee > 0 ? qBusiness.PayfritFee : 0.05;
businessFeePercent = 0.05; // 5% business pays to Payfrit (always)
businessFeePercent = customerFeePercent; // Business fee mirrors customer fee
cardFeePercent = 0.029; // 2.9% Stripe fee
cardFeeFixed = 0.30; // $0.30 Stripe fixed fee
payfritCustomerFee = subtotal * customerFeePercent;
payfritBusinessFee = subtotal * businessFeePercent;
totalBeforeCardFee = subtotal + tax + tip + deliveryFee + payfritCustomerFee;
cardFee = (totalBeforeCardFee * cardFeePercent) + cardFeeFixed;
totalCustomerPays = totalBeforeCardFee + cardFee;
// Stripe charges 2.9% + $0.30 on the TOTAL charged, not the pre-fee amount.
// To fully pass through Stripe fees: charge = (net + fixed) / (1 - percent)
totalCustomerPays = (totalBeforeCardFee + cardFeeFixed) / (1 - cardFeePercent);
cardFee = totalCustomerPays - totalBeforeCardFee;
// Convert to cents for Stripe
totalAmountCents = round(totalCustomerPays * 100);
@ -131,6 +144,9 @@ try {
httpService.setUsername(stripeSecretKey);
httpService.setPassword("");
// IDEMPOTENCY: Prevent duplicate PaymentIntents on client retry
httpService.addParam(type="header", name="Idempotency-Key", value="pi-order-#orderID#");
httpService.addParam(type="formfield", name="amount", value=totalAmountCents);
httpService.addParam(type="formfield", name="currency", value="usd");
httpService.addParam(type="formfield", name="automatic_payment_methods[enabled]", value="true");
@ -166,6 +182,20 @@ try {
abort;
}
// Store expected amount and PaymentIntent ID for webhook verification
queryExecute("
UPDATE Orders
SET ExpectedAmountCents = :expectedCents,
StripePaymentIntentID = :piID
WHERE ID = :orderID
", {
expectedCents: totalAmountCents,
piID: piData.id,
orderID: orderID
}, { datasource: "payfrit" });
writeLog(file="stripe_webhooks", text="PaymentIntent #piData.id# created for order #orderID# (#totalAmountCents# cents, idempotency key: pi-order-#orderID#)");
// Fees are calculated dynamically, not stored in DB
// Link PaymentIntent to worker payout ledger (if a ledger row exists for this order's task)

View file

@ -233,18 +233,19 @@ try {
refundAmount = eventData.amount_refunded / 100;
if (paymentIntentID != "") {
// FIX: Use correct column name (was OrderStripePaymentIntentID)
qOrder = queryTimed("
SELECT ID FROM Orders
WHERE OrderStripePaymentIntentID = :paymentIntentID
WHERE StripePaymentIntentID = :paymentIntentID
", { paymentIntentID: paymentIntentID });
if (qOrder.recordCount > 0) {
queryTimed("
UPDATE Orders
SET PaymentStatus = 'refunded',
OrderRefundAmount = :refundAmount,
OrderRefundedOn = NOW()
WHERE OrderStripePaymentIntentID = :paymentIntentID
RefundAmount = :refundAmount,
RefundedOn = NOW()
WHERE StripePaymentIntentID = :paymentIntentID
", {
paymentIntentID: paymentIntentID,
refundAmount: refundAmount
@ -261,16 +262,17 @@ try {
paymentIntentID = eventData.payment_intent ?: "";
if (paymentIntentID != "") {
// FIX: Use correct column name (was OrderStripePaymentIntentID)
qOrder = queryTimed("
SELECT ID, BusinessID FROM Orders
WHERE OrderStripePaymentIntentID = :paymentIntentID
WHERE StripePaymentIntentID = :paymentIntentID
", { paymentIntentID: paymentIntentID });
if (qOrder.recordCount > 0) {
queryTimed("
UPDATE Orders
SET PaymentStatus = 'disputed'
WHERE OrderStripePaymentIntentID = :paymentIntentID
WHERE StripePaymentIntentID = :paymentIntentID
", { paymentIntentID: paymentIntentID });
// Create a task for the dispute