Store & payments

The store sells products (one-time or recurring) whose deliverables become entitlements the moment Stripe confirms payment. Entitlements are pushed to Discord, FiveM or applied internally through delivery jobs.

The only path that grants anything

Customer → Stripe Checkout → Stripe → POST /webhooks/stripe (signature verified)
        → webhook_events (unique event id) → job stripe.process_event
        → order PAID → entitlements ACTIVE → delivery_jobs → Discord / FiveM / internal

The browser landing on /store/success never grants anything. It only shows the order state.

Products

Dashboard → Store → New product.

FieldNotes
Kindone_time or recurring (monthly / yearly via Stripe Billing)
Price / currencyStored in cents. Changing price is audited (product.price_changed).
Minimum ageBuyers need age_verified or identity_verified status
StockBlank = unlimited (informational; Stripe does not enforce it)
CommunityOptional: attach the product to one community page
DeliverablesOne or more perks granted on payment (below)

Deliverables

TypeFieldsDelivered by
discord_roleguild ID, role IDDiscord bot (DISCORD_BOT_TOKEN); needs the buyer's Discord linked (users.discord_id)
fivem_groupgroup / ACE name, optional server IDThe FiveM resource polls /api/v1/servers/{id}/entitlements/pending and acks each grant
creditsamountInternal, applied immediately
permissionkey (e.g. priority_queue)Internal, applied immediately

Deliverables are stored as JSON on the product and copied onto each order item at checkout, so later product edits do not change what a past order owes.

Stripe setup

  1. Create a Stripe account, copy the secret key to STRIPE_SECRET_KEY and the publishable key to STRIPE_PUBLISHABLE_KEY.
  2. Add a webhook endpoint in Stripe → Developers → Webhooks:
  • URL: https://krunc.com/webhooks/stripe
  • Events: checkout.session.completed, invoice.paid, invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted, charge.refunded, charge.dispute.created
  • Copy the signing secret to STRIPE_WEBHOOK_SECRET.
  1. Enable the Customer portal in Stripe (Settings → Billing → Customer portal) so /store/portal/{tenantId} works.

Without STRIPE_SECRET_KEY the app runs a mock provider: checkout redirects straight to /store/success?mock=1, nothing is ever marked paid, and the dashboard shows a warning banner.

Local webhook testing

stripe listen --forward-to https://krunc.com/webhooks/stripe
stripe trigger checkout.session.completed

Test cards: 4242 4242 4242 4242 (success), 4000 0000 0000 9995 (declined), 4000 0000 0000 0341 (attaches, then fails on renewal).

Webhook handling

  • Signature: Stripe-Signature header, HMAC-SHA256 over "{t}.{payload}", 5-minute tolerance.
  • Idempotency: webhook_events (provider, event_id) is unique. A replayed event returns 200 {"duplicate":true} and does nothing.
  • Every handler re-checks state before writing (paid orders stay paid, revoked entitlements are not re-revoked), so re-processing is safe.
  • Processing is queued (stripe.process_event). With QUEUE_DRIVER=sync it runs inline; with database run php bin/worker.
EventEffect
checkout.session.completedOrder → paid, Stripe customer saved, subscription mirrored, entitlements + delivery jobs created, receipt emailed, order.paid automation trigger
invoice.paidSubscription active, period extended, lapsed entitlements re-granted
invoice.payment_failedSubscription past_due, "payment failed" email (perks stay until Stripe gives up)
customer.subscription.updatedMirror status / cancel-at-period-end; revoke on canceled/unpaid
customer.subscription.deletedSubscription canceled, entitlements revoked, subscription.canceled trigger
charge.refundedOrder refunded (full refunds revoke entitlements)
charge.dispute.createdOrder disputed, entitlements revoked, owners/admins notified

Refunds

Dashboard → Orders → open the order → Refund & revoke (admin/owner only). This calls Stripe's refund API, cancels any subscription, marks the order refunded and revokes the entitlements. Stripe's own charge.refunded event arrives later and is a no-op.

Delivery jobs

StatusMeaning
pendingCreated; for FiveM: waiting for the game server to poll
waitingRetrying later (e.g. Discord not linked, Discord API error) — scheduler store.retry_deliveries runs every 10 min
deliveredApplied
ackedFiveM server confirmed it applied the grant
failedGave up after 10 attempts — use Re-deliver on the Entitlements page
canceledGrant was revoked before it was applied

Manual grants and revokes (Entitlements page) go through the same pipeline and are written to the audit log.

Coupons

Dashboard → Coupons. Percent or fixed amount, optional max uses / expiry. Applied at checkout as a one-time Stripe coupon; orders.discount_cents and coupon_code record what was applied.