payfrit-works/api/stripe/createPaymentIntent.cfm
John Mizerek d225133c68 Add menu builder required selection UI and fix portal issues
Menu Builder - Required Selections:
- Added "Selection Rules" section for modifier groups
- Required (Yes/No) dropdown to mark if customer must select an option
- Max Selections input (0 = unlimited) to limit selections
- Visual "Required" badge (red) and "Max X" badge in modifier list
- Updated saveFromBuilder.cfm to persist ItemRequiresChildSelection
  and ItemMaxNumSelectionReq to database

Portal Fixes:
- Fixed menu-builder link to include BASE_PATH for local dev
- Fixed stats.cfm to not reference non-existent Categories table
- Menu items count now uses ItemParentItemID > 0 (not ItemCategoryID)

Stripe Configuration:
- Added api/config/stripe.cfm for centralized Stripe key management
- Supports test/live mode switching
- Fee configuration variables (5% customer, 5% business, 2.9% + $0.30 card)

Payment Intent API:
- Updated createPaymentIntent.cfm with proper fee structure
- Customer pays: subtotal + tax + tip + 5% Payfrit fee + card processing
- Platform receives 10% total (5% from customer + 5% from business)
- Saves fee breakdown to order record

Beacon Management:
- Updated switchBeacons.cfm to move beacons between businesses
- Currently configured: Big Dean's (27) -> In-N-Out (17)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 01:56:12 -08:00

168 lines
5.9 KiB
Text

<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
/**
* Create Payment Intent for Order (v2 - with Payfrit fee structure)
*
* Fee Structure:
* - Customer pays: Subtotal + tax + tip + 5% Payfrit fee + card processing (2.9% + $0.30)
* - Restaurant receives: Subtotal - 5% Payfrit fee + tax + tip
* - Payfrit receives: 10% of subtotal (5% from customer + 5% from restaurant)
*
* POST: {
* BusinessID: int,
* OrderID: int,
* Subtotal: number (order subtotal in dollars, before fees),
* Tax: number (tax amount in dollars),
* Tip: number (optional, in dollars),
* CustomerEmail: string (optional)
* }
*/
response = { "OK": false };
try {
requestData = deserializeJSON(toString(getHttpRequestData().content));
businessID = val(requestData.BusinessID ?: 0);
orderID = val(requestData.OrderID ?: 0);
subtotal = val(requestData.Subtotal ?: 0);
tax = val(requestData.Tax ?: 0);
tip = val(requestData.Tip ?: 0);
customerEmail = requestData.CustomerEmail ?: "";
if (businessID == 0) {
response["ERROR"] = "BusinessID is required";
writeOutput(serializeJSON(response));
abort;
}
if (orderID == 0) {
response["ERROR"] = "OrderID is required";
writeOutput(serializeJSON(response));
abort;
}
if (subtotal <= 0) {
response["ERROR"] = "Invalid subtotal";
writeOutput(serializeJSON(response));
abort;
}
// Use test keys
stripeSecretKey = application.stripeSecretKey ?: "sk_test_LfbmDduJxTwbVZmvcByYmirw";
if (stripeSecretKey == "") {
response["ERROR"] = "Stripe is not configured";
writeOutput(serializeJSON(response));
abort;
}
// Get business Stripe account
qBusiness = queryExecute("
SELECT BusinessStripeAccountID, BusinessStripeOnboardingComplete, BusinessName
FROM Businesses
WHERE BusinessID = :businessID
", { businessID: businessID }, { datasource: "payfrit" });
if (qBusiness.recordCount == 0) {
response["ERROR"] = "Business not found";
writeOutput(serializeJSON(response));
abort;
}
// For testing, allow orders even without Stripe Connect setup
hasStripeConnect = qBusiness.BusinessStripeOnboardingComplete == 1 && len(trim(qBusiness.BusinessStripeAccountID)) > 0;
// ============================================================
// FEE CALCULATION
// ============================================================
customerFeePercent = 0.05; // 5% customer pays to Payfrit
businessFeePercent = 0.05; // 5% business pays to Payfrit
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 + payfritCustomerFee;
cardFee = (totalBeforeCardFee * cardFeePercent) + cardFeeFixed;
totalCustomerPays = totalBeforeCardFee + cardFee;
// Convert to cents for Stripe
totalAmountCents = round(totalCustomerPays * 100);
totalPlatformFeeCents = round((payfritCustomerFee + payfritBusinessFee) * 100);
// Create PaymentIntent
httpService = new http();
httpService.setMethod("POST");
httpService.setUrl("https://api.stripe.com/v1/payment_intents");
httpService.setUsername(stripeSecretKey);
httpService.setPassword("");
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");
if (hasStripeConnect) {
httpService.addParam(type="formfield", name="application_fee_amount", value=totalPlatformFeeCents);
httpService.addParam(type="formfield", name="transfer_data[destination]", value=qBusiness.BusinessStripeAccountID);
}
httpService.addParam(type="formfield", name="metadata[order_id]", value=orderID);
httpService.addParam(type="formfield", name="metadata[business_id]", value=businessID);
httpService.addParam(type="formfield", name="description", value="Order ###orderID# at #qBusiness.BusinessName#");
if (customerEmail != "") {
httpService.addParam(type="formfield", name="receipt_email", value=customerEmail);
}
result = httpService.send().getPrefix();
piData = deserializeJSON(result.fileContent);
if (structKeyExists(piData, "error")) {
response["ERROR"] = piData.error.message;
writeOutput(serializeJSON(response));
abort;
}
// Save payment intent and fee info to order
queryExecute("
UPDATE Orders
SET OrderStripePaymentIntentID = :paymentIntentID,
OrderTipAmount = :tipAmount,
OrderPayfritFee = :payfritFee,
OrderCardFee = :cardFee,
OrderTotalCharged = :totalCharged
WHERE OrderID = :orderID
", {
paymentIntentID: piData.id,
tipAmount: tip,
payfritFee: payfritCustomerFee + payfritBusinessFee,
cardFee: cardFee,
totalCharged: totalCustomerPays,
orderID: orderID
}, { datasource: "payfrit" });
response["OK"] = true;
response["CLIENT_SECRET"] = piData.client_secret;
response["PAYMENT_INTENT_ID"] = piData.id;
response["PUBLISHABLE_KEY"] = application.stripePublishableKey ?: "pk_test_sPBNzSyJ9HcEPJGC7dSo8NqN";
response["FEE_BREAKDOWN"] = {
"SUBTOTAL": subtotal,
"TAX": tax,
"TIP": tip,
"PAYFRIT_FEE": payfritCustomerFee,
"CARD_FEE": cardFee,
"TOTAL": totalCustomerPays
};
response["STRIPE_CONNECT_ENABLED"] = hasStripeConnect;
} catch (any e) {
response["ERROR"] = e.message;
response["DETAIL"] = e.detail ?: "";
}
writeOutput(serializeJSON(response));
</cfscript>