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>
168 lines
5.9 KiB
Text
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>
|