Step 3
Create and persist the mapped UPI order
Read the trusted total from the authenticated Shopify webhook.
Persist a one-to-one mapping before making the external call, reuse
the same `client_txn_id` after a timeout, and send the resulting
hosted URL using a channel permitted by the merchant’s Shopify and
communications setup.
async function createUpiForShopifyOrder(
order: ShopifyOrderWebhook,
) {
const amountPaise = decimalToPaise(order.current_total_price);
const clientTxnId = `SHOP-${order.id}`;
// insertOrLoadPaymentMapping has UNIQUE constraints on:
// (shop_domain, shopify_order_id), client_txn_id, gateway_order_id
const mapping = await insertOrLoadPaymentMapping({
shopifyOrderId: String(order.admin_graphql_api_id),
clientTxnId,
amountPaise,
currency: order.currency,
});
if (mapping.paymentUrl) return mapping;
if (order.currency !== "INR") throw new Error("UPI order must be INR");
const response = await fetch(
`${mustGetEnv("VG_BASE_URL")}/api/v1/create_order`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": mustGetEnv("VG_API_KEY"),
},
body: JSON.stringify({
client_txn_id: clientTxnId,
amount: paiseToRupees(amountPaise),
p_info: `Shopify order ${order.name}`,
customer_name:
[order.customer?.first_name, order.customer?.last_name]
.filter(Boolean)
.join(" "),
customer_email: order.email,
customer_mobile: order.phone,
redirect_url:
`${mustGetEnv("APP_PUBLIC_URL")}/shopify/orders/${order.id}`,
callback_url:
`${mustGetEnv("APP_PUBLIC_URL")}/webhooks/vyapargateway`,
}),
signal: AbortSignal.timeout(10_000),
},
);
const result = await response.json() as CreateOrderResponse;
if (!response.ok || !result.status) {
throw new Error("Unable to create UPI payment order");
}
if (
result.data.client_txn_id !== clientTxnId ||
result.data.currency !== "INR"
) {
throw new Error("Gateway order mismatch");
}
return saveGatewayMapping(mapping.id, {
gatewayOrderId: result.data.order_id,
paymentUrl: result.data.payment_url,
expiresAt: result.data.expires_at,
});
}