@@ -163,6 +163,13 @@ export default class CloudPaymentsWebhooks {
163163 const recurrentPaymentSettings = data . cloudPayments ?. recurrent ;
164164 let promoPricing ;
165165
166+ /**
167+ * Record promo usage before applying paid benefits.
168+ *
169+ * /pay runs after CloudPayments has accepted the charge, but workspace plan
170+ * must not be changed if promo usage cannot be stored. Otherwise a transient
171+ * DB/limit error would grant a discounted plan without consuming the promo.
172+ */
166173 if ( data . promo && ! data . isCardLinkOperation ) {
167174 try {
168175 const promoCodeService = new PromoCodeService ( context . factories ) ;
@@ -301,6 +308,36 @@ export default class CloudPaymentsWebhooks {
301308 return ;
302309 }
303310
311+ if ( data . promo && ! data . isCardLinkOperation ) {
312+ try {
313+ const promoCodeService = new PromoCodeService ( req . context . factories ) ;
314+ const promoPricing = await promoCodeService . getPricingForPromoCodeId (
315+ data . promo . id ,
316+ data . userId ,
317+ data . workspaceId ,
318+ tariffPlan
319+ ) ;
320+
321+ await promoCodeService . createUsage ( {
322+ promoCode : promoPricing . promoCode ,
323+ userId : data . userId ,
324+ workspaceId : workspace . _id ,
325+ planId : tariffPlan . _id ,
326+ benefitType : promoPricing . benefitType ,
327+ originalAmount : promoPricing . originalAmount ,
328+ finalAmount : promoPricing . finalAmount ,
329+ discountAmount : promoPricing . discountAmount ,
330+ utm : data . promo . utm ,
331+ } ) ;
332+ } catch ( e ) {
333+ const error = e as Error ;
334+
335+ this . sendError ( res , PayCodes . SUCCESS , `[Billing / Pay] Failed to record promo usage: ${ error . toString ( ) } ` , body ) ;
336+
337+ return ;
338+ }
339+ }
340+
304341 try {
305342 await businessOperation . setStatus ( BusinessOperationStatus . Confirmed ) ;
306343
@@ -334,34 +371,6 @@ export default class CloudPaymentsWebhooks {
334371 return ;
335372 }
336373
337- if ( data . promo && ! data . isCardLinkOperation ) {
338- try {
339- const promoCodeService = new PromoCodeService ( req . context . factories ) ;
340- const promoPricing = await promoCodeService . getPricingForPromoCodeId (
341- data . promo . id ,
342- data . userId ,
343- data . workspaceId ,
344- tariffPlan
345- ) ;
346-
347- await promoCodeService . createUsage ( {
348- promoCode : promoPricing . promoCode ,
349- userId : data . userId ,
350- workspaceId : workspace . _id ,
351- planId : tariffPlan . _id ,
352- benefitType : promoPricing . benefitType ,
353- originalAmount : promoPricing . originalAmount ,
354- finalAmount : promoPricing . finalAmount ,
355- discountAmount : promoPricing . discountAmount ,
356- utm : data . promo . utm ,
357- } ) ;
358- } catch ( e ) {
359- const error = e as Error ;
360-
361- console . error ( `[Billing / Pay] Failed to record promo usage: ${ error . toString ( ) } ` , body ) ;
362- }
363- }
364-
365374 // let accountId = workspace.accountId;
366375
367376 /*
@@ -824,10 +833,29 @@ status: ${body.Status}`
824833 */
825834 if ( body . Data ) {
826835 const parsedData = JSON . parse ( body . Data || '{}' ) as WebhookData ;
836+ const checksumData = checksumService . parseAndVerifyChecksum ( parsedData . checksum ) ;
837+
838+ /**
839+ * Treat checksum as the source of truth for billing intent.
840+ *
841+ * Widget Data is client-controlled, so it must not override signed fields like
842+ * workspaceId, tariffPlanId, userId, shouldSaveCard, or promo id. Only
843+ * CloudPayments recurrent settings are accepted from Data because they are
844+ * validated separately against server-side pricing in /check.
845+ */
846+ if ( 'isCardLinkOperation' in checksumData ) {
847+ return {
848+ ...checksumData ,
849+ tariffPlanId : '' ,
850+ shouldSaveCard : false ,
851+ ...( parsedData . cloudPayments ? { cloudPayments : parsedData . cloudPayments } : { } ) ,
852+ } ;
853+ }
827854
828855 return {
829- ...checksumService . parseAndVerifyChecksum ( parsedData . checksum ) ,
830- ...parsedData ,
856+ ...checksumData ,
857+ ...( parsedData . cloudPayments ? { cloudPayments : parsedData . cloudPayments } : { } ) ,
858+ isCardLinkOperation : false ,
831859 } ;
832860 }
833861
0 commit comments