import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.ts' import { createServiceRoleClient } from '../_shared/quota.ts' import { calcSubscriptionPeriod, generateOrderId, getPaypleConfig, parsePaypleTimestamp, paypleAuth, paypleBilling, PaypleBillingError, paypleLookupBillingKey, payplePaymentEventDigest, payplePaymentEventId, payplePayerNumber, PaypleConfigurationError, TIER_GOODS_NAME, TIER_PRICE, } from '../_shared/payple.ts' import { chargeMatchesOrder, decideReservation, isBillingKeyOwnedBy, parseCheckoutRequest, planCheckoutFailure, } from './checkout-policy.ts' interface ApplyResult { applied?: boolean duplicate?: boolean reason?: string } function jsonResponse(body: Record, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } Deno.serve(async (req: Request) => { const preflight = handleCorsPreflightRequest(req) if (preflight) return preflight if (req.method !== 'POST') return jsonResponse({ error: 'method_not_allowed' }, 405) let operationId: string | null = null let providerOrderId: string | null = null let externalChargeCompleted = false const serviceClient = createServiceRoleClient() try { const user = await requireUser(req) const input = parseCheckoutRequest(await req.json()) if (!input) return jsonResponse({ error: 'invalid_request' }, 400) // Configuration is validated before reserving state or contacting Payple. // There are deliberately no bundled/test credential fallbacks. const config = getPaypleConfig() const idempotencyKey = input.idempotencyKey ?? `payple-checkout:${crypto.randomUUID()}` const orderId = generateOrderId(user.id) providerOrderId = orderId const { data: reservationData, error: reservationError } = await serviceClient.rpc( 'reserve_payment_provider_operation', { p_user_id: user.id, p_provider: 'payple', p_operation_type: 'checkout', p_requested_tier: input.tier, p_idempotency_key: idempotencyKey, p_provider_order_id: orderId, // Bind the reserved order to this billing key before any charge. The // PUSERINFO lookup below still verifies its authenticated user owner. p_provider_resource_id: input.payerId, }, ) if (reservationError) throw new Error('payment_reservation_failed') // The reservation is the single serialization point: it rejects an // in-flight operation, another provider's ownership, and an active paid // Payple period ('subscription_already_active') before any charge. const reservation = decideReservation(reservationData) if (reservation.kind === 'reject') { return jsonResponse(reservation.body, reservation.status) } operationId = reservation.operationId const price = TIER_PRICE[input.tier] const goodsName = TIER_GOODS_NAME[input.tier] const billingKeyAuth = await paypleAuth(config, { payWork: 'PUSERINFO' }) const billingKey = await paypleLookupBillingKey(config, billingKeyAuth, input.payerId) const expectedPayerNumbers = [user.id, await payplePayerNumber(user.id)] if (!isBillingKeyOwnedBy(billingKey, input.payerId, expectedPayerNumbers)) { throw new Error('payple_billing_key_owner_mismatch') } const auth = await paypleAuth(config, { simpleFlag: true }) const billingResult = await paypleBilling(config, auth, { payerId: input.payerId, amount: price, orderId, goodsName, }) externalChargeCompleted = true const { error: chargedError } = await serviceClient.rpc('mark_payment_provider_operation', { p_operation_id: operationId, p_state: 'charged', p_external_reference: billingResult.PCD_PAY_OID || orderId, p_error_code: null, }) if (chargedError) throw new Error('payment_operation_update_failed') if (!chargeMatchesOrder(billingResult, { orderId, amount: price, payerId: input.payerId })) { throw new Error('payple_charge_response_mismatch') } const eventDate = billingResult.PCD_PAY_TIME ? parsePaypleTimestamp(billingResult.PCD_PAY_TIME) : new Date() const { start, end } = calcSubscriptionPeriod(eventDate) const { data: applyData, error: applyError } = await serviceClient.rpc( 'apply_payment_provider_event', { p_user_id: user.id, p_provider: 'payple', p_event_id: payplePaymentEventId(orderId), p_event_created_at: eventDate.toISOString(), p_event_type: 'payment.completed', p_payload_digest: await payplePaymentEventDigest({ orderId, payerId: input.payerId, payType: 'card', amount: price, }), p_provider_resource_id: input.payerId, p_tier: input.tier, p_status: 'active', p_entitled: true, p_current_period_start: start, p_current_period_end: end, p_cancel_at: null, p_auto_renewing: true, p_provider_customer_id: input.payerId, p_provider_order_id: orderId, p_store_product_id: null, p_store_purchase_id: null, p_operation_id: operationId, }, ) if (applyError) throw new Error('entitlement_persistence_failed') const applied = applyData as ApplyResult | null if (!applied?.applied && !applied?.duplicate) { return jsonResponse({ error: 'payment_requires_reconciliation', reason: applied?.reason ?? 'entitlement_not_applied', order_id: orderId, }, 409) } return jsonResponse({ success: true, tier: input.tier, order_id: orderId, amount: price, }) } catch (error) { const plan = planCheckoutFailure( { hasOperation: operationId !== null, externalChargeCompleted, chargeOutcomeUnknown: error instanceof PaypleBillingError && !error.definitive, configurationError: error instanceof PaypleConfigurationError, }, error instanceof PaypleConfigurationError ? error.code : 'payple_not_configured', ) if (operationId && plan.mark?.state === 'external_created') { await serviceClient.rpc('mark_payment_provider_operation', { p_operation_id: operationId, p_state: 'external_created', p_external_reference: providerOrderId, p_error_code: null, }) } else if (operationId && plan.mark?.state === 'failed') { await serviceClient.rpc('mark_payment_provider_operation', { p_operation_id: operationId, p_state: 'failed', p_external_reference: null, p_error_code: plan.mark.errorCode, }) } if (error && typeof error === 'object' && 'status' in error && 'message' in error) { return authErrorResponse(error as AuthError, corsHeaders) } return jsonResponse({ error: plan.error }, plan.status) } })