07 / discounts · coupons · promotions · quote tokens

One selected discount.
Separate time controls.

Current working-tree behavior inspected on 8 Oct 2026. Follow each offer from creation to preview, checkout, redemption and recurring settlement. Pending defects are drawn as existing behavior, with concrete numbers and source citations.

9 focused diagramsUSD / INRNo stackingSignup countsD11 implemented in source

Reading the evidence: every diagram node and detail table points to source. Links open repository files; the displayed suffix gives the source line. These are local, uncommitted working-tree findings, rather than a claimed committed snapshot or gateway execution. Free trial claims are a separate lifecycle; the paid-plan discount paths are shown here.

01 / current implementation

Plan discounts and the full-cycle price

The plan’s monthly, quarterly and yearly percentage reductions are already baked into its stored per-asset rates. A coupon or promotion is then calculated against that rate’s full cycle.

Plan discounts and the full-cycle priceAdmin sets interval rate: Base rate + interval discount · USD and INR are separate → Discount, then tax: Rate × (1 − plan discount) · Then × (1 + tax); round 2dp → Build full-cycle base: Stored rate × assets × months · month 1 · quarter 3 · year 12 → Calculate discount: Percentage of full-cycle base · Or fixed once; cap at base01 / frontendAdmin sets interval rateBase rate + interval discountUSD and INR are separate02 / backendDiscount, then taxRate × (1 − plan discount)Then × (1 + tax); round 2dp03 / backendBuild full-cycle baseStored rate × assets × monthsmonth 1 · quarter 3 · year 1204 / backendCalculate discountPercentage of full-cycle baseOr fixed once; cap at base
siem-admin-new/app/dashboard/plans/page.tsx:202 · siem-admin-new/app/dashboard/plans/page.tsx:207 · siem-server-new/src/services/subscription.service.ts:52 · siem-server-new/src/services/subscription.service.ts:120
ControlActual calculation / exampleSource
Plan interval discountA base ₹250 rate with 20% plan discount becomes ₹200 before tax. With 18% tax, the stored rate is ₹236. A later 10% coupon is calculated from the tax-inclusive cycle base.siem-admin-new/app/dashboard/plans/page.tsx:207
Full-cycle baseAt a stored ₹200 rate: 5 assets monthly = ₹1,000. At a quarterly rate of ₹200: 5 × ₹200 × 3 = ₹3,000. Interval rates can differ; the selected interval’s own stored rate is used.siem-server-new/src/services/subscription.service.ts:52
Percentage coupon / promotion20% of ₹1,000 = ₹200. The amount is capped at the cycle base and rounded to two decimal places.siem-server-new/src/services/subscription.service.ts:120
Fixed coupon / promotion₹500 against a ₹3,000 quarterly cycle gives ₹2,500. The ₹500 is deducted once per cycle, rather than once per asset or each month within that cycle.siem-server-new/src/services/subscription.service.ts:120
CurrencyFixed discounts must match USD or INR. Percentage discounts have no currency amount. ₹1,000 fixed against a ₹600 base produces a ₹600 discount and a zero charge.siem-server-new/src/services/subscription.service.ts:238
Tax and gateway amountNo second tax addition occurs at checkout. Major units become cents/paise with Math.round(amount × 100). Razorpay’s mandate ceiling uses the full base plus headroom, before the coupon deduction.siem-server-new/src/services/subscription.service.ts:653 · siem-server-new/src/services/subscription.service.ts:78 · siem-server-new/src/services/subscription.service.ts:666
02 / current implementation

Manual coupons: public code, account use and limits

A Super Admin creates a reusable code. It is not assigned to an account. Any eligible authenticated customer can attempt it; the configured global limit and one-use-per-user rule apply.

Manual coupons: public code, account use and limitsCreate manual coupon: Code, type, value, duration · Dates, plans, currency, cap → Customer enters code: Normalize uppercase · Apply reserves a slot → Checkout revalidates: Dates, active, plan, currency · Global cap + prior user use → Attach at checkout: Slot moves to the signup · Payment page 30 minutes01 / frontendCreate manual couponCode, type, value, durationDates, plans, currency, cap02 / frontendCustomer enters codeNormalize uppercaseApply reserves a slot03 / securityCheckout revalidatesDates, active, plan, currencyGlobal cap + prior user use04 / databaseAttach at checkoutSlot moves to the signupPayment page 30 minutes
siem-server-new/src/services/discount.service.ts:11 · siem-server-new/src/services/discountSelection.service.ts:69 · siem-server-new/src/services/couponBooking.service.ts:38 · siem-server-new/src/services/couponBooking.service.ts:62
RuleCurrent behaviorSource
AdministrationCreate/list/toggle/delete coupon endpoints require isSuperAdmin. Manual codes are 3–50 characters, uppercase letters, digits, underscore or hyphen; value must be positive and percentage cannot exceed 100.siem-server-new/src/routes/admin.routes.ts:89 · siem-server-new/src/validators/discountSchema.ts:5
EligibilityCoupon exists, is active, starts no later than now, has not passed its end date, matches a restricted plan and fixed currency, has global capacity, and has no active booking or successful redemption for this user. A calculated zero discount is rejected.siem-server-new/src/services/subscription.service.ts:226
Plan restrictionnull, an empty array, and a non-array value are interpreted as all plans. A non-empty array restricts the coupon to its listed plan IDs.siem-server-new/src/services/subscription.service.ts:135
Global usagemaxRedemptions = null means unlimited global uses. A finite cap checks redemptionCount + bookingCount under the coupon lock before reserving a slot.siem-server-new/src/services/subscription.service.ts:246
Per-user usageThe unique (couponId, userId) pair prevents the same user redeeming the same coupon again, including on another subscription. The coupon row lock and booking counter also enforce the global cap across different users.siem-server-new/prisma/schema.prisma:778
DurationcyclesTotal = null means the stored discount continues every cycle. A finite number includes signup as cycle 1 of the allowance.siem-server-new/src/services/discount.service.ts:25 · siem-server-new/src/payment/processor/PaymentEventProcessor.ts:423
Deleting / togglingA coupon with checkout or redemption history cannot be deleted; it must be deactivated. Deactivation blocks future eligibility checks and does not erase a subscription’s copied amount.siem-server-new/src/services/discount.service.ts:46 · siem-server-new/src/services/subscription.service.ts:98
Code vs quote JWTA manual coupon code is the separate account-independent mechanism. A quote JWT locates a contact and its linked coupon; these are distinct inputs, not interchangeable token formats.siem-server-new/prisma/schema.prisma:744 · siem-server-new/src/utils/auth.ts:88

Selected only: only an applied coupon code holds a slot; removing it or choosing the quote or a promotion releases the slot. A selected promotion creates no CouponRedemption. siem-server-new/src/services/discountSelection.service.ts:69

03 / current implementation

Promotions: automatic offers without a code

Promotions share the same fixed/percentage amount calculation but have their own active date range and no coupon redemption ledger.

Promotions: automatic offers without a codeSuper Admin creates offer: Name, occasion, type, value · Dates, plans, currency, cycles → Find running promotions: isActive and start ≤ now ≤ end · Match plan and fixed currency → List as choices: Largest amount first · Customer may pick any → Copy the selected one: Store promotionId and amount · No global redemption counter01 / frontendSuper Admin creates offerName, occasion, type, valueDates, plans, currency, cycles02 / backendFind running promotionsisActive and start ≤ now ≤ endMatch plan and fixed currency03 / backendList as choicesLargest amount firstCustomer may pick any04 / databaseCopy the selected oneStore promotionId and amountNo global redemption counter
siem-server-new/src/services/discount.service.ts:77 · siem-server-new/src/services/subscription.service.ts:159 · siem-server-new/src/services/subscription.service.ts:175 · siem-server-new/src/services/subscription.service.ts:633
QuestionCurrent answerSource
What does the customer enter?No code. Every running promotion for the plan and currency is shown as a choice beside the quote; the customer selects one. Without a quote the largest promotion is selected by default.siem-server-new/src/services/discountSelection.service.ts:286
What makes it eligible?Active flag, inclusive start/end timestamps, plan applicability and fixed-currency match. Non-positive calculated amounts are skipped.siem-server-new/src/services/subscription.service.ts:159
Are there usage limits?The Promotion model has no maxRedemptions or redemptionCount fields and no CouponRedemption relation. One-use-per-user validation belongs to coupons.siem-server-new/prisma/schema.prisma:808
How are several promotions ordered?Largest per-cycle amount first; equal amounts by name. A selected promotion that ends before checkout falls back to the quote, else the next running promotion.siem-server-new/src/services/discountSelection.service.ts:286
What if a new promotion starts later?Existing renewals use the subscription’s stored discount; they do not search for a new promotion. The active range controls selection for a new purchase.siem-server-new/src/services/subscription.service.ts:98
What if the promotion ends or is switched off?A subscription that already copied its amount and cycle allowance keeps them until its own allowance runs out. Promotion expiry is not subscription discount expiry.siem-server-new/src/services/discount.service.ts:110 · siem-server-new/src/services/subscription.service.ts:698
04 / current implementation

Quote tokens: contact link, custom plan and linked coupon

The JWT contains a contact ID and a custom_quote type. It carries no discount value. The server loads the contact’s plan and coupon to calculate the discount.

Quote tokens: contact link, custom plan and linked couponAdmin sends quote: Pick plan and contact · Optional discount on custom plan → Sign contact JWT: contact_id + custom_quote · Expiry set by admin or none → Create linked QUOTE code: Only when discount > 0 · One redemption; quoted plan only → Email link and save SENT: Link includes ?token=JWT · Contact stores plan + couponId01 / frontendAdmin sends quotePick plan and contactOptional discount on custom plan02 / securitySign contact JWTcontact_id + custom_quoteExpiry set by admin or none03 / databaseCreate linked QUOTE codeOnly when discount > 0One redemption; quoted plan only04 / externalEmail link and save SENTLink includes ?token=JWTContact stores plan + couponId
siem-server-new/src/services/plan.service.ts:736 · siem-server-new/src/utils/auth.ts:83 · siem-server-new/src/services/plan.service.ts:796 · siem-server-new/src/services/plan.service.ts:846
Quote propertyActual ruleSource
Discount on a custom planA custom quote can have no coupon. A quote on a standard plan requires a positive discount, and that plan must be active and paid.siem-server-new/src/services/plan.service.ts:759
Coupon created for an offerRandom QUOTE- code; fixed/percentage value; quoted plan only; maxRedemptions = 1; validity from creation to the quote's expiry (none when the quote never expires); finite cycles or null for LIFETIME.siem-server-new/src/services/plan.service.ts:796
Where the percentage is calculatedJWT → contact_id → ContactUs.couponId → Coupon.discountType and discountValue → percentage × selected full-cycle base, or the fixed amount. The JWT itself is not an amount.siem-server-new/src/services/subscription.service.ts:572 · siem-server-new/src/services/subscription.service.ts:617 · siem-server-new/src/services/subscription.service.ts:120
What the quote email pricesThe email lists the per-asset price of each interval. A percentage discount is applied in that price; a fixed discount is stated separately as an amount off the total of each billing cycle, because it does not scale with assets. D16 resolved.siem-server-new/src/services/plan.service.ts:818
D16 example₹200 per asset monthly with a fixed ₹100 offer: the email shows “₹200.00 / asset” and “₹100.00 off the total of each billing cycle”. Checkout with 5 assets is ₹1,000 − ₹100 = ₹900, matching what the email states.siem-server-new/src/services/plan.service.ts:820 · siem-server-new/src/services/subscription.service.ts:669
Email duration wordingThe quote email says “N billing cycles, including signup” (or “Every billing cycle”). A three-cycle offer covers signup plus two renewals, the same wording as the calculator.siem-server-new/src/services/plan.service.ts:834 · siem-server-new/src/payment/processor/PaymentEventProcessor.ts:423 · siem-client-new/components/reusable/price-calculator/index.tsx:1115
Quote currency — D58 resolvedThe contact form stores the inquiry's company registration country. It fixes the quote currency (IN: INR, others: USD); the admin does not choose it. Checkout through the quote link is fixed to the same country, so a quote for a company registered in India is priced, emailed and charged in INR.siem-server-new/src/services/plan.service.ts:806 · siem-server-new/src/services/subscription.service.ts:238

Separate controls to remember

ControlExpires / ends whenEffect
QuoteThe admin's chosen date and time (entered in local time, stored as UTC), or never. The quote link (JWT) carries no expiry of its own.After ContactUs.quoteExpiresAt the quote link, its custom plan and its discount are refused. It does not expire a stored subscription discount. siem-server-new/src/utils/auth.ts:83
Quote-created couponvalidTo = the quote's expiry (none when the quote never expires).The quote and its coupon end together. siem-server-new/src/services/plan.service.ts:796
Discount selection35 minutes after the customer's last action, never beyond 24 hours from the first selection.Holds the selected discount (and a coupon code's slot) until checkout. siem-server-new/src/services/couponBooking.service.ts:13
Stored subscription discountN successful discounted settlements including signup, or null = every cycle.Controls future subtraction after purchase. Coupon validTo and promotion endDate are not read by renewal’s amount helper. siem-server-new/src/services/subscription.service.ts:98 · siem-server-new/src/payment/processor/PaymentEventProcessor.ts:428

A custom quote without a coupon still has the JWT expiry. getActivePlans only populates quoteExpiresAt from an eligible coupon, so that response field does not communicate every quote token’s expiry. siem-server-new/src/services/plan.service.ts:514 · siem-server-new/src/services/plan.service.ts:557

05 / current implementation

Quote link: customer routing, plan visibility and enforcement gaps

A quote link selects the quoted plan. The public list excludes other custom plans. Token checks differ between public validation, plan loading and checkout; the diagram does not treat them as a single universal guard.

Quote link: customer routing, plan visibility and enforcement gapsCustomer opens link: Registered subscriber → sign in · New buyer → onboarding → Load quoted plan: JWT + current stored token · Reject ACCEPTED / EXPIRED → Verify quote on server: JWT, current link, owner email · SENT quote + selected plan → Accept after settlement: First successful payment · Includes internal ₹0 settlement01 / frontendCustomer opens linkRegistered subscriber → sign inNew buyer → onboarding02 / securityLoad quoted planJWT + current stored tokenReject ACCEPTED / EXPIRED03 / backendVerify quote on serverJWT, current link, owner emailSENT quote + selected plan04 / databaseAccept after settlementFirst successful paymentIncludes internal ₹0 settlement
siem-client-new/app/portal/(me)/buy-new-subscription/page.tsx:43 · siem-server-new/src/services/plan.service.ts:516 · siem-server-new/src/services/subscription.service.ts:572 · siem-server-new/src/services/subscription.service.ts:553
BoundaryEnforced checks and current gapsSource
Public quote validationVerifies JWT, contact/plan and quote status. The email-match guard only runs when userEmail is supplied; the public controller calls it without userEmail. validateQuote does not compare the token with contact.quoteToken.siem-server-new/src/controllers/plan.controller.ts:107 · siem-server-new/src/services/plan.service.ts:888 · siem-server-new/src/services/plan.service.ts:914
Public plan listgetActivePlans compares the JWT with the contact’s current stored quoteToken, rejects accepted/expired quotes, requires an active quoted plan, and adds only that plan to the standard public list. It returns raw rates plus quote discount metadata.siem-server-new/src/services/plan.service.ts:508
Authenticated checkoutThe authenticated user is used for coupon redemption. Checkout verifies the JWT, requires SENT, checks the current stored token and selected plan, and matches contact.email to the authenticated user.siem-server-new/src/services/subscription.service.ts:572
Quote coupon as a typed codeQUOTE coupons additionally require their linked SENT contact, current valid quote JWT and the authenticated account’s email. Manual codes remain account-independent.siem-server-new/src/services/subscription.service.ts:204 · siem-server-new/prisma/schema.prisma:744
Expired / invalid JWT in checkoutAn expired or invalid quote JWT rejects checkout. A custom plan requires a valid quote token.siem-server-new/src/services/subscription.service.ts:574
RevocationrevokeQuote deactivates the linked coupon and clears the contact’s token/coupon link. A subscription’s already copied discount is still used by resolveCycleChargeAmount.siem-server-new/src/services/plan.service.ts:1057 · siem-server-new/src/services/subscription.service.ts:98
Acceptance timing — D48 resolved in sourceThe first successful settlement transaction marks the quote ACCEPTED. A failed or abandoned checkout does not accept it. Internal ₹0 settlement follows the same rule.siem-server-new/src/payment/processor/PaymentEventProcessor.ts:438 · siem-server-new/src/services/subscription.service.ts:581
06 / current implementation

The customer selects one discount

The customer chooses the quote, one running promotion, or a coupon code. Nothing stacks and nothing competes. The server holds the selection for 35 minutes, restarted by every action, never beyond 24 hours.

The customer selects one discountShow the options: quote and running promotions · Quote selected by default → Coupon code applied: Slot reserved at once · Stays a choice beside the others → Hold 35 minutes: Restarted by each action · 24 hours at most → Checkout re-prices: Ended discount falls back · Changed price needs confirming01 / frontendShow the optionsQuote and running promotionsQuote selected by default02 / databaseCoupon code appliedSlot reserved at onceStays a choice beside the others03 / backendHold 35 minutesRestarted by each action24 hours at most04 / securityCheckout re-pricesEnded discount falls backChanged price needs confirming
siem-server-new/src/services/discountSelection.service.ts:69 · siem-server-new/src/services/couponBooking.service.ts:38 · siem-server-new/src/services/subscription.service.ts:433 · siem-client-new/components/reusable/price-calculator/index.tsx:392
SituationImplemented outcomeSource
Quote 25% and promotion 15% both available on ₹1,000Both are shown; the quote is selected by default (charge ₹750). The customer can switch to the promotion (charge ₹850).siem-server-new/src/services/discountSelection.service.ts:125
Two promotions runningEach is a separate choice, largest per-cycle amount first; without a quote the largest is selected by default.siem-server-new/src/services/discountSelection.service.ts:286
Coupon code appliedThe slot is reserved at once under the coupon lock; quote and promotions are hidden until Remove coupon, which returns to quote, else the largest promotion. An invalid or fully booked code shows its error and changes nothing.siem-server-new/src/services/couponBooking.service.ts:38
Selected discount's end date passes before CheckoutFallback: quote → largest promotion; promotion → quote → next promotion; coupon → quote → largest promotion; else no discount. The customer sees “Your selected discount is no longer available.”siem-server-new/src/services/discountSelection.service.ts:196
Admin switches the selected discount off while the selection livesThe customer keeps it until checkout. A discount selected afterwards must be active.siem-server-new/src/services/discountSelection.service.ts:196
Price at Checkout differs from the price shown409 PRICE_CHANGED with the new price; the customer confirms before paying. The client's amount is only compared, never charged.siem-server-new/src/services/subscription.service.ts:444
Unusable quote discount (wrong currency, expired coupon) — D47, D58 resolvedNot offered; the selection uses a promotion or no discount. A custom plan stays purchasable while its quote itself is usable.siem-server-new/src/services/discountSelection.service.ts:69
Public pricing page (not signed in)Shows the largest running promotion; nothing is held and no coupon field is shown.siem-server-new/src/services/discountSelection.service.ts:171
07 / current implementation

Stored discount: signup, renewals, retries and expiry

Company registration country fixes currency at checkout: India uses INR; other supported countries use USD. Both fixed and percentage offers become a currency amount copied to the subscription at checkout. Normal renewals and retries subtract that stored amount while the subscription’s discount allowance remains.

Stored discount: signup, renewals, retries and expiryCopy selected discount: Amount + cyclesTotal + ID · Initial cyclesUsed = 0 → Resolve amount owed: Pending price or locked price · Subtract while used < total → Successful settlement: Including ₹0 internal settlement · Increment used by one → Allowance exhausted: Finite: restore full cycle charge · null: discount continues01 / databaseCopy selected discountAmount + cyclesTotal + IDInitial cyclesUsed = 002 / backendResolve amount owedPending price or locked priceSubtract while used < total03 / backendSuccessful settlementIncluding ₹0 internal settlementIncrement used by one04 / backendAllowance exhaustedFinite: restore full cycle chargenull: discount continues
siem-server-new/src/services/subscription.service.ts:671 · siem-server-new/src/services/subscription.service.ts:98 · siem-server-new/src/payment/processor/PaymentEventProcessor.ts:423 · siem-server-new/src/services/subscription.service.ts:106
Path / eventAmount and cycle behaviorSource
Three-cycle exampleBase ₹1,000, stored discount ₹200, cyclesTotal = 3: signup ₹800 (used 1), renewal 1 ₹800 (used 2), renewal 2 ₹800 (used 3), next renewal ₹1,000. A failed attempt between these successes consumes no extra allowance.siem-server-new/src/payment/processor/PaymentEventProcessor.ts:423 · siem-server-new/src/services/subscription.service.ts:98
Percentage after a price change20% of ₹1,000 stores ₹200. If an admin pushes ₹1,200 while allowance remains, renewal is ₹1,200 − ₹200 = ₹1,000. It does not recalculate to ₹960. This is the current stored-amount design; no new percentage policy is assumed.siem-server-new/src/services/subscription.service.ts:120 · siem-server-new/src/services/subscription.service.ts:698 · siem-server-new/src/services/subscription.service.ts:105
Coupon expires / promotion ends after purchaseThe original definition’s dates and active flag are not queried by the normal cycle amount helper. The copied amount and cycle allowance remain effective.siem-server-new/src/services/subscription.service.ts:98
Signup is fully discountedNo gateway call. The service records a ₹0 invoice/payment, increments discountCyclesUsed once, provisions the customer and lands the paid subscription in PENDING_SETUP for admin activation.siem-server-new/src/services/subscription.service.ts:719
Fully discounted renewalThe billing worker emits an internal PAYMENT_SUCCEEDED with a deterministic cycle ID. The processor records settlement and consumes a discount cycle.siem-server-new/src/bullmq/workers/billingCycleWorker.ts:171 · siem-server-new/src/payment/processor/PaymentEventProcessor.ts:428
Discount ends and no payment method was ever registeredThe next positive cycle requires provider, token and locked price. With missing payment setup, the worker moves the subscription to PAST_DUE and schedules grace actions.siem-server-new/src/bullmq/workers/billingCycleWorker.ts:188
Scheduled renewal, scheduled retry, manual retryThese paths call resolveCycleChargeAmount and preserve the stored discount while it remains active. A zero amount is internally settled rather than sent to the gateway.siem-server-new/src/bullmq/workers/billingCycleWorker.ts:159 · siem-server-new/src/bullmq/workers/retryPaymentWorker.ts:46 · siem-server-new/src/services/subscription.service.ts:1900
Payment-method recovery — D10 resolvedsetupPaymentMethodForExistingSubscription charges resolveCycleChargeAmount (pushed or locked price minus the running discount), sends that discount as the server snapshot and keeps lockedPriceAmount. Company registration country fixes gateway and currency; recovery cannot switch them.siem-server-new/src/services/subscription.service.ts:2143 · siem-server-new/src/services/subscription.service.ts:2160 · siem-server-new/src/services/subscription.service.ts:2182
Upgrade / trial-to-paid conversionThese are separate change paths, not a re-run of new-checkout coupon/promotion selection. The normal recurring-discount formula above should not be read as a guarantee that upgrade proration includes that deduction.siem-server-new/src/services/subscription.service.ts:1333 · siem-server-new/src/payment/processor/PaymentEventProcessor.ts:148

Gateway boundary: Stripe USD and Razorpay INR receive the server’s computed charge. Coupon dates, promotion selection and remaining discount cycles are controlled by SIEM. The initial Razorpay mandate ceiling is the undiscounted cycle base plus headroom. siem-server-new/src/services/subscription.service.ts:49 · siem-server-new/src/services/subscription.service.ts:666 · siem-server-new/src/services/subscription.service.ts:1159

08 / current implementation

D11: coupon slot from apply to redemption

Applying a coupon code reserves one slot at once, on the database clock, under the coupon row lock. Checkout attaches the slot to the signup. It becomes a successful redemption only after the first payment settles; it is released when the selection expires or the gateway shows no payment can still complete.

Coupon slot from apply to redemptionApply coupon: Lock coupon row · Reserve slot or refuse → Hold with selection: 35 minutes per action · 24 hours at most → Checkout attaches: Payment page 30 minutes · Slot checked at 35 minutes → Redeem or release: Settled: redeem once · No payment possible: release01 / backendApply couponLock coupon rowReserve slot or refuse02 / databaseHold with selection35 minutes per action24 hours at most03 / externalCheckout attachesPayment page 30 minutesSlot checked at 35 minutes04 / securityRedeem or releaseSettled: redeem onceNo payment possible: release
siem-server-new/src/services/couponBooking.service.ts:38 · siem-server-new/src/services/couponBooking.service.ts:62 · siem-server-new/src/services/couponBooking.service.ts:182
Customer actionServer decisionConcrete result
One slot left; customer A applies the coupon, customer B applies it a minute laterA's apply reserves the slot under the coupon lock. B's apply finds no free slot.A sees the discount and keeps it. B sees “All remaining redemptions are currently booked. Please try again later.” immediately, not at checkout.
A sits idle 34:59, then changes the asset countThe selection call re-prices the coupon and restarts the 35-minute hold, never beyond 24 hours from the first apply.The slot stays with A.
A leaves for more than 35 minutesThe every-minute maintenance job releases the unattached slot.Another customer can apply the coupon.
A clicks CheckoutThe billing transaction attaches the slot to the new signup; the payment page closes after 30 minutes and the slot is checked 35 minutes after checkout.Stripe expires_at; Razorpay popup timeout 1800 s.
Stripe first charge declinedThe failure closes the checkout and releases the slot in the same transaction.A starts over at today's price.
Razorpay popup closed without payingAt the check the server reads the order's payments: captured → settle; in progress → keep; failed within Razorpay's 3-day late-authorisation window → keep until the window ends; none → release.No slot is freed while money can still arrive. A late payment on a closed checkout is honored (no refunds).
Discount covers ₹1,000 entirelyInternal ₹0 settlement redeems the slot and consumes one discounted cycle.No Stripe or Razorpay interaction.

Scale: per-coupon row locks, coupon → user lock order for selection and checkout, coupon → subscription for settlement, bounded conflict retries, indexed scans in batches of 100. Gateway calls happen outside database transactions. Ordinary 25-hour cleanup excludes coupon subscriptions and in-flight signup payments.

siem-server-new/src/services/couponBooking.service.ts:130 · siem-server-new/src/payment/providers/RazorpayProvider.ts:105 · siem-server-new/src/payment/providers/StripeProvider.ts:76

09 / current implementation

D11: one settlement, one discounted cycle

Settlement locks the coupon and subscription, then rechecks the invoice and current billing cycle inside the transaction. The actual discount travelled in server-created payment metadata, rather than being inferred from a later subscription edit.

One settlement and one discounted cycleLock and reload: Coupon then subscription · Read current cycle + invoice → Apply settlement once: Invoice, payment, redemption · Count actual discount once → Recheck next delivery: Same payment: no second write · Settled cycle: no extra use → Recover follow-up jobs: Provisioning, emails, next job · Retry without recounting discount01 / backendLock and reloadCoupon then subscriptionRead current cycle + invoice02 / databaseApply settlement onceInvoice, payment, redemptionCount actual discount once03 / databaseRecheck next deliverySame payment: no second writeSettled cycle: no extra use04 / securityRecover follow-up jobsProvisioning, emails, next jobRetry without recounting discount
siem-server-new/src/payment/processor/PaymentEventProcessor.ts:360 · siem-server-new/src/payment/billingTransaction.ts:4
DeliveryDatabase resultExample
First successful paymentInvoice, unique payment, BOOKED → REDEEMED, counter transfer and subscription status commit together.Booked 1 / redeemed 99 becomes booked 0 / redeemed 100.
Same payment delivered twice concurrentlyThe second transaction reloads and finds the existing invoice. It returns without another payment or discount increment.A three-cycle offer with used = 1 becomes used = 2 after this renewal.
Another payment for a settled cycleThe money is recorded with its own unique external ID; the settled cycle is not applied again.It does not consume the third discounted cycle.
Subscription edited while payment settlesThe charged discount snapshot determines whether an allowance was consumed.A payment charged with ₹0 discount consumes no discount cycle even if an admin subsequently adds a discount.
Process stops after settlement commitsThe invoice retains a pending follow-up. A bounded indexed recovery scan queues provisioning, IAM, emails and the next-cycle schedule again.Retries reuse existing membership and do not recount the discount. External emails remain at-least-once.

siem-server-new/src/payment/processor/PaymentEventProcessor.ts:494 · siem-server-new/src/payment/processor/PaymentEventProcessor.ts:508 · siem-server-new/prisma/schema.prisma:474

Source implementation reviewed; schema application, Prisma generation and gateway/database execution are separate prerequisites. No tests were created or run.