@@ -10,6 +10,7 @@ import WorkspaceModel from '../models/workspace';
1010import { ContextFactories } from '../types/graphql' ;
1111import type { Utm } from '@hawk.so/types' ;
1212import type { PaymentPromoData } from '../billing/types/paymentData' ;
13+ import { sanitizeUtmParams } from '../utils/utm/utm' ;
1314
1415const PROMO_CODE_REGEXP = / ^ [ A - Z 0 - 9 _ - ] + $ / ;
1516const DEFAULT_MIN_FINAL_PRICE = 1 ;
@@ -347,9 +348,11 @@ function validateBenefitStructure(benefit: PromoCodeBenefit): void {
347348 * @returns promo reference for payment checksum
348349 */
349350export function buildPaymentPromoData ( promoCodeId : string , utm ?: Utm ) : PaymentPromoData {
351+ const sanitizedUtm = sanitizeUtmParams ( utm ) ;
352+
350353 return {
351354 id : promoCodeId ,
352- ...( utm && Object . keys ( utm ) . length > 0 ? { utm } : { } ) ,
355+ ...( sanitizedUtm ? { utm : sanitizedUtm } : { } ) ,
353356 } ;
354357}
355358
@@ -502,53 +505,41 @@ export default class PromoCodeService {
502505 throw new PromoCodeError ( PromoCodeErrorCode . Invalid , 'Grant plan is unavailable' ) ;
503506 }
504507
508+ const now = new Date ( ) ;
509+
510+ try {
511+ await workspace . updatePlanHistory ( workspace . tariffPlanId . toString ( ) , now , userId ) ;
512+ await workspace . updateLastChargeDate ( now ) ;
513+ await workspace . changePlan ( plan . _id ) ;
514+ } catch ( error ) {
515+ if ( error instanceof PromoCodeError ) {
516+ throw error ;
517+ }
518+
519+ throw new PromoCodeError ( PromoCodeErrorCode . ApplyFailed , 'Grant plan apply failed' ) ;
520+ }
521+
505522 try {
506- const now = new Date ( ) ;
507-
508- /**
509- * Reserve usage before granting the plan.
510- *
511- * This makes promo usage a precondition for the benefit: if limits are exhausted
512- * or the insert fails, workspace state is not changed.
513- */
514- const usage = await this . createUsage ( {
523+ await this . createUsage ( {
515524 promoCode,
516525 userId,
517526 workspaceId : workspace . _id ,
518527 planId : plan . _id ,
519528 benefitType : promoCode . benefit . type ,
520529 utm,
521530 } ) ;
522-
523- try {
524- await workspace . updatePlanHistory ( workspace . tariffPlanId . toString ( ) , now , userId ) ;
525- await workspace . updateLastChargeDate ( now ) ;
526- await workspace . changePlan ( plan . _id ) ;
527- } catch ( error ) {
528- try {
529- await this . factories . promoCodeUsagesFactory . deleteById ( usage . _id ) ;
530- } catch ( rollbackError ) {
531- console . error ( 'Failed to rollback promo usage after grant_plan apply failure' , rollbackError ) ;
532- }
533-
534- throw error ;
535- }
536-
537- return plan ;
538531 } catch ( error ) {
539- if ( error instanceof PromoCodeError ) {
540- throw error ;
541- }
542-
543- throw new PromoCodeError ( PromoCodeErrorCode . ApplyFailed , 'Grant plan apply failed' ) ;
532+ console . error ( '[PromoCode] Failed to record promo usage after grant_plan apply' , error ) ;
544533 }
534+
535+ return plan ;
545536 }
546537
547538 /**
548539 * Creates usage after successful payment or before immediate grant_plan apply.
549540 *
550- * Unique indexes on promoCodeId + userId/workspaceId make this method the durable
551- * reservation point. Callers should grant the promo benefit only after it succeeds .
541+ * Unique indexes on promoCodeId + userId/workspaceId enforce one usage per user/workspace.
542+ * Usage is recorded after plan change in CloudPayments /pay and grant_plan apply .
552543 *
553544 * @param params - usage creation params
554545 * @returns created promo usage
@@ -566,6 +557,8 @@ export default class PromoCodeService {
566557 } ) : Promise < PromoCodeUsageModel > {
567558 await this . validateUsageLimits ( params . promoCode , params . userId , params . workspaceId ) ;
568559
560+ const utm = sanitizeUtmParams ( params . utm ) ;
561+
569562 try {
570563 return await this . factories . promoCodeUsagesFactory . create ( {
571564 promoCodeId : params . promoCode . _id ,
@@ -577,7 +570,7 @@ export default class PromoCodeService {
577570 finalAmount : params . finalAmount ,
578571 discountAmount : params . discountAmount ,
579572 appliedAt : new Date ( ) ,
580- ...( params . utm && Object . keys ( params . utm ) . length > 0 ? { utm : params . utm } : { } ) ,
573+ ...( utm ? { utm } : { } ) ,
581574 } ) ;
582575 } catch ( error ) {
583576 if ( ( error as { code ?: number } ) . code === 11000 ) {
0 commit comments