Billing Engine

Multi-gateway payment processing supporting Stripe, PayPal, and Paymob with self-service checkout, payment links, customer portal, and webhook-driven state synchronization.

The Billing Engine powers SCRIPE's subscription lifecycle with a provider-agnostic payment architecture. It supports three payment gateways (Stripe, PayPal, Paymob) and three subscription modes: Self-Service (tenant picks a plan and pays via the configured gateway), Contact Sales (admin generates a payment link for enterprise deals), and Manual Assignment (admin assigns a plan without payment). All payment events are synchronized via gateway-specific webhooks.

IPaymentGateway Abstraction

The payment system is built on an IPaymentGateway interface in Core.Application with an IPaymentGatewayResolver that dynamically selects the correct provider per tenant. Three implementations are active: StripePaymentGateway (global, recurring, billing portal), PayPalPaymentGateway (international, OAuth2-based), and PaymobPaymentGateway (MENA region, card tokenization). Gateways are registered as Keyed DI services and resolved at runtime.

Core.Application/Abstractions/Payment/IPaymentGateway.csC#
public interface IPaymentGateway
{
    Task<CreateCheckoutSessionResult> CreateCheckoutSessionAsync(CreateCheckoutSessionRequest request);
    Task<CreatePaymentLinkResult> CreatePaymentLinkAsync(CreatePaymentLinkRequest request);
    Task<CreatePortalSessionResult> CreatePortalSessionAsync(string customerId, string returnUrl);
    Task<CancelSubscriptionResult> CancelSubscriptionAsync(string subscriptionId, bool cancelAtPeriodEnd);
}

Three Subscription Modes

Every tenant onboarding follows one of three paths. The mode is chosen by whether the edition has IsSelfServiceEnabled or IsContactSalesOnly set.

Self-Service (Automated)

The tenant selects an edition and billing cycle in the admin panel. SCRIPE resolves the correct payment gateway (via tenant override or global default), creates a checkout session, sets TenantSubscription to PendingPayment, and redirects the tenant to the gateway's hosted checkout page. On successful payment, the gateway webhook fires and SCRIPE activates the subscription automatically.

Tenant selects edition + billing cycle
POST /billing/checkout-session
Stripe Checkout Session created
TenantSubscription → PendingPayment
Tenant redirected to Stripe Checkout
checkout.session.completed webhook
HMAC signature verified
TenantSubscription → Active ✅

Contact Sales (Admin-Assisted)

For enterprise or custom-priced deals, the admin creates a subscription with a payment link via the resolved gateway. SCRIPE generates a reusable payment link URL, which the admin sends to the client. The same webhook flow activates the subscription once the client pays.

Admin negotiates deal with client
POST /billing/payment-link
Stripe Payment Link created
Admin sends URL to client
Client pays via Stripe
Same webhook flow as self-service

Manual Assignment (Skip Payment)

For partners, internal accounts, or free trials, the admin assigns an edition directly. No gateway interaction occurs — the subscription is set to Active immediately. Use this for Free editions, internal tenants, or manually negotiated deals.

HTTP
POST /api/v1/subscriptions/assign
Authorization: Bearer <admin-jwt>

{
  "tenantId": "encrypted-id",
  "editionId": "encrypted-id",
  "subscriptionType": "Monthly",
  "currency": "USD"
  // No Stripe interaction — instant activation
}

Webhook Handlers

Each gateway has its own webhook endpoint with provider-specific signature verification. Stripe uses HMAC-SHA256 at POST /api/stripe-webhooks, PayPal uses transmission signature verification at POST /api/paypal-webhooks, and Paymob uses HMAC-SHA512 at POST /api/paymob-webhooks. All handlers dispatch to SCRIPE mediator commands for processing.

Event TypeHandler Action
checkout.session.completedActivate subscription, create Invoice + PaymentTransaction
invoice.paidRecord renewal payment, create new subscription row
invoice.payment_failedMove to PastDue, start dunning, send warning email
customer.subscription.updatedSync status (active/past_due/canceled/unpaid)
customer.subscription.deletedCancel subscription, trigger fallback edition
charge.refundedCreate refund record, void invoice
payment_intent.succeededRecord successful payment intent

Idempotency

All webhook handlers are idempotent — processing the same event twice has no side effects. Gateway transaction IDs are checked before creating new records. This protects against at-least-once delivery guarantees from all providers.

Configuration

Each gateway is configured in appsettings.json under its own section (Stripe, PayPal, Paymob). The PaymentGateways section controls which gateways are enabled, which is the global default, and whether tenants can override the gateway selection. Use sandbox/test keys for development.

appsettings.jsonJSON
{
  "Stripe": {
    "SecretKey": "sk_test_...",
    "PublishableKey": "pk_test_...",
    "WebhookSecret": "whsec_...",
    "SuccessUrl": "https://app.scripe.com/billing/success",
    "CancelUrl": "https://app.scripe.com/billing/cancel"
  }
}

Billing Portal (Stripe Only)

Once a tenant has an active Stripe subscription, they can manage billing via Stripe's hosted Customer Portal. This feature is Stripe-exclusive — PayPal and Paymob subscriptions show gateway-specific management guidance instead. The UI automatically hides the portal button for non-Stripe gateways.

Edition Self-Service Fields

Two fields on the Edition entity control which payment mode is available: IsSelfServiceEnabled (tenant can check out without contacting sales) and IsContactSalesOnly (the Checkout button shows 'Contact Sales' and triggers the payment link flow instead).

FieldTypeEffect
IsSelfServiceEnabledbooltrue = tenant can self-checkout. false = must contact sales.
IsContactSalesOnlybooltrue = checkout button triggers payment link flow, not checkout session.

Zero-Decimal Currency Handling

SCRIPE includes a CurrencyHelper that correctly converts amounts for zero-decimal currencies (JPY, KWD, BHD, etc.). Regular currencies (USD, EUR, SAR, etc.) are multiplied by 100 to convert to smallest units. Zero-decimal currencies are passed as-is. This applies across all gateways.

API Endpoints

The BillingController exposes 5 endpoints under /api/v1/billing, plus gateway management at /api/v1/payment-gateways:

MethodEndpointDescriptionAuth
POST/api/v1/billing/tenants/{id}/checkout-sessionCreate checkout session via the resolved payment gateway (supports GatewayOverride parameter)🔒 Required
POST/api/v1/billing/tenants/{id}/payment-linkGenerate payment link via the resolved gateway (Contact Sales flow)🔒 Required
POST/api/v1/billing/tenants/{id}/portal-sessionCreate Stripe Customer Portal session (Stripe-only, returns error for other gateways)🔒 Required
POST/api/v1/billing/tenants/{id}/cancel-stripeCancel gateway subscription (resolves the correct gateway from the subscription's PaymentGateway field)🔒 Required
GET/api/v1/billing/dashboardGet revenue dashboard (MRR, ARR, churn, trends — aggregated across all gateways)🔒 Required
POST/api/stripe-webhooksStripe webhook receiver — no auth, HMAC-verified🔒 Required