Skip to content

TASK #00000 : Update discount value issue fix#703

Open
mahajanmahesh935 wants to merge 2 commits intotekdi:aspire-leadersfrom
mahajanmahesh935:CouponIssue
Open

TASK #00000 : Update discount value issue fix#703
mahajanmahesh935 wants to merge 2 commits intotekdi:aspire-leadersfrom
mahajanmahesh935:CouponIssue

Conversation

@mahajanmahesh935
Copy link
Copy Markdown
Collaborator

@mahajanmahesh935 mahajanmahesh935 commented Mar 26, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Inactive coupons can now be reused and reactivated instead of rejected on duplicate codes.
    • Stripe syncing now reliably handles existing Stripe coupons and promotion codes, improving error recovery.
    • Coupon updates always trigger Stripe synchronization regardless of prior sync state.
  • Improvements

    • Webhook handling now recognizes expired checkout sessions and records a clear failed-reason for transactions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Walkthrough

Reused inactive coupon records instead of rejecting duplicates, added Stripe immutability checks and promotion-code handling, reworked Stripe sync to handle resource conflicts and promotion-code lifecycle, added webhook event parsing for checkout.session.expired, and centralized failed-webhook reason assignment in payment webhook handling.

Changes

Cohort / File(s) Summary
Coupon service
src/payments/services/coupon.service.ts
createCoupon now reuses inactive DB rows by overwriting fields and triggering Stripe sync. Added stripeCouponMatchesEntity and promotionCodeCouponId helpers. syncCouponToStripe updated to handle resource_already_exists by fetching existing coupons, validating immutables, deactivating mismatched promotion codes, and always creating a new promotion code; updateCoupon now triggers Stripe sync whenever Stripe is configured.
Stripe webhook parsing
src/payments/providers/stripe/stripe.provider.ts
Added explicit handling for checkout.session.expired in parseWebhookEvent: maps session id, payment_intent, sets status 'failed', resolves currency/amount, and copies metadata.
Payment webhook failure handling
src/payments/services/payment.service.ts
Refactored handleWebhook to precompute failedWebhookReason (including special-case for expired checkout sessions) and use it when creating/updating transactions instead of inline conditional logic.

Sequence Diagram

sequenceDiagram
    actor User
    participant Service as Coupon Service
    participant DB as Database
    participant Stripe as Stripe API

    User->>Service: createCoupon(dto)
    Service->>DB: Query existing coupon by code
    alt Coupon exists & inactive
        Service->>DB: Update coupon fields
        Service->>Service: syncCouponToStripe()
        Service->>Stripe: Create Stripe coupon (desiredCouponId)
        alt Stripe returns resource_already_exists
            Service->>Stripe: Retrieve existing Stripe coupon
            Service->>Service: stripeCouponMatchesEntity()
            alt Immutable fields match
                Service->>Stripe: Create promotion code
                Service->>DB: Save stripePromoCodeId
            else Immutable fields differ
                Service->>Stripe: Get existing promotion code (if any)
                Service->>Stripe: Deactivate conflicting promotion code
                Service->>Stripe: Create new Stripe coupon
                Service->>Stripe: Create promotion code
                Service->>DB: Save new stripePromoCodeId
            end
        else Stripe coupon creation succeeds
            Service->>Stripe: Create promotion code
            Service->>DB: Save stripePromoCodeId
        end
    else Coupon exists & active
        Service->>User: BadRequestException
    else Coupon doesn't exist
        Service->>DB: Create new coupon
        Service->>Service: syncCouponToStripe()
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title mentions 'Update discount value issue fix' but the changes primarily address coupon creation logic, Stripe webhook event handling, and failure reason refactoring—not specifically a discount value update issue. Revise the title to accurately reflect the main changes, such as 'Handle inactive coupon reuse and improve Stripe sync logic' or 'Add support for checkout.session.expired and refactor failure handling'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/payments/services/coupon.service.ts`:
- Around line 56-68: Reusing the inactive coupon row overwrites terms but keeps
coupon.id, currentRedemptions and existing CouponRedemption rows, which allows a
reactivated coupon to appear already consumed; when updating an inactive
existing (the variable existing in this block) you must reset usage state or
clear past redemptions: either create a fresh coupon row instead of reusing the
same id, or if you must reuse the row, set existing.currentRedemptions = 0 and
remove or mark associated CouponRedemption records as stale/archived so
validateCoupon() checks reflect the new terms; update related logic that
references coupon.id and CouponRedemption to ensure per-user counts and total
redemptions start from zero on reactivation.
- Around line 217-237: When desiredCouponId exists but its immutable fields
differ, avoid always creating a new Stripe coupon; first try to derive/reuse the
active coupon referenced by coupon.stripePromoCodeId (retrieve the promotion
code via this.stripe.promotionCodes.retrieve or retrieve the promo code's coupon
and confirm it matches paramsForNewCoupon) and persist that Stripe coupon ID to
the DB (e.g., coupon.stripeCouponId) before deactivating or creating
replacements. Update the flow in the branch that uses
this.stripe.coupons.retrieve(desiredCouponId) /
this.stripe.promotionCodes.update(...) so it: 1) if mismatch, attempt to fetch
the coupon linked to coupon.stripePromoCodeId and use it when it matches, 2)
only deactivate the promo and create paramsForNewCoupon when no active matching
coupon can be found, and 3) persist the chosen active Stripe coupon ID back to
the entity so subsequent updateCoupon() runs reuse it instead of cycling
replacements; apply the same change to the similar block later in the file.
- Around line 69-86: When reactivating a coupon the old stripePromoCodeId can
remain on the returned object if Stripe sync fails; ensure the coupon's
stripePromoCodeId is cleared before saving so we never return a stale promo id.
In the reactivation flow in coupon.service.ts (references: stripePromoCodeId,
couponRepository.save, syncCouponToStripe, savedCoupon) set
existing.stripePromoCodeId = null when dto.stripePromoCodeId is undefined (or
explicitly override savedCoupon.stripePromoCodeId = null before returning) and
persist that state so a failed sync won't leave a previous Stripe promo id on
the coupon.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0b8f3290-5e7c-466f-9c61-d32efc88493c

📥 Commits

Reviewing files that changed from the base of the PR and between 8f2d213 and 720c7c5.

📒 Files selected for processing (1)
  • src/payments/services/coupon.service.ts

Comment on lines +56 to +68
// Same code is reserved by an inactive row (unique constraint). Reuse the row:
// stable couponId for reporting/redemptions, new terms from this payload.
existing.contextType = dto.contextType;
existing.contextId = dto.contextId;
existing.discountType = dto.discountType;
existing.discountValue = dto.discountValue;
existing.currency = dto.currency || 'USD';
existing.countryId = dto.countryId ?? null;
existing.isActive = dto.isActive ?? true;
existing.validFrom = dto.validFrom ? new Date(dto.validFrom) : null;
existing.validTill = dto.validTill ? new Date(dto.validTill) : null;
existing.maxRedemptions = dto.maxRedemptions ?? null;
existing.maxRedemptionsPerUser = dto.maxRedemptionsPerUser ?? null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reusing this row also reuses the old redemption history.

This branch rewrites the coupon definition but keeps the same coupon.id, currentRedemptions, and existing CouponRedemption rows. validateCoupon() later checks both total and per-user usage by that ID, so a reactivated coupon can come back already partially or fully exhausted.

🧰 Tools
🪛 ESLint

[error] 62-62: Replace 'USD' with "USD"

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/payments/services/coupon.service.ts` around lines 56 - 68, Reusing the
inactive coupon row overwrites terms but keeps coupon.id, currentRedemptions and
existing CouponRedemption rows, which allows a reactivated coupon to appear
already consumed; when updating an inactive existing (the variable existing in
this block) you must reset usage state or clear past redemptions: either create
a fresh coupon row instead of reusing the same id, or if you must reuse the row,
set existing.currentRedemptions = 0 and remove or mark associated
CouponRedemption records as stale/archived so validateCoupon() checks reflect
the new terms; update related logic that references coupon.id and
CouponRedemption to ensure per-user counts and total redemptions start from zero
on reactivation.

Comment on lines +69 to +86
if (dto.stripePromoCodeId !== undefined) {
existing.stripePromoCodeId = dto.stripePromoCodeId ?? null;
}

const savedCoupon = await this.couponRepository.save(existing);
if (this.stripe && !dto.stripePromoCodeId) {
try {
await this.syncCouponToStripe(savedCoupon);
} catch (error) {
this.logger.warn(
`Failed to sync reactivated coupon ${savedCoupon.id} to Stripe: ${error.message}`,
);
}
}
this.logger.log(
`Reactivated inactive coupon: ${savedCoupon.couponCode} (${savedCoupon.id})`,
);
return savedCoupon;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't return a reactivated coupon with the previous Stripe promo ID still attached.

Line 73 saves the new terms before Stripe sync, and the catch block only logs. If this inactive row already had stripePromoCodeId, the method returns that old value unchanged when sync fails. In src/payments/services/payment.service.ts:48-147, checkout only re-syncs when the field is missing, so the previous Stripe promotion code can be reused for the new coupon terms.

💡 Safer fallback
+      const previousStripePromoCodeId = existing.stripePromoCodeId;
+      if (dto.stripePromoCodeId === undefined) {
+        existing.stripePromoCodeId = null;
+      }
       const savedCoupon = await this.couponRepository.save(existing);
       if (this.stripe && !dto.stripePromoCodeId) {
         try {
+          savedCoupon.stripePromoCodeId = previousStripePromoCodeId;
           await this.syncCouponToStripe(savedCoupon);
         } catch (error) {
+          await this.couponRepository.update(savedCoupon.id, {
+            stripePromoCodeId: null,
+          });
           this.logger.warn(
             `Failed to sync reactivated coupon ${savedCoupon.id} to Stripe: ${error.message}`,
           );
         }
       }
🧰 Tools
🪛 ESLint

[error] 79-79: Delete ,

(prettier/prettier)


[error] 84-84: Delete ,

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/payments/services/coupon.service.ts` around lines 69 - 86, When
reactivating a coupon the old stripePromoCodeId can remain on the returned
object if Stripe sync fails; ensure the coupon's stripePromoCodeId is cleared
before saving so we never return a stale promo id. In the reactivation flow in
coupon.service.ts (references: stripePromoCodeId, couponRepository.save,
syncCouponToStripe, savedCoupon) set existing.stripePromoCodeId = null when
dto.stripePromoCodeId is undefined (or explicitly override
savedCoupon.stripePromoCodeId = null before returning) and persist that state so
a failed sync won't leave a previous Stripe promo id on the coupon.

Comment on lines 217 to +237
if (error.code === 'resource_already_exists') {
stripeCoupon = await this.stripe.coupons.retrieve(
stripeCouponParams.id as string,
);
const existing = await this.stripe.coupons.retrieve(desiredCouponId);
if (this.stripeCouponMatchesEntity(existing, coupon)) {
stripeCoupon = existing;
} else {
this.logger.log(
`Stripe coupon ${desiredCouponId} exists but does not match DB; creating new Stripe coupon (immutable fields changed)`,
);
if (coupon.stripePromoCodeId) {
try {
await this.stripe.promotionCodes.update(coupon.stripePromoCodeId, {
active: false,
});
} catch (deactErr) {
if (deactErr.code !== 'resource_missing') {
throw deactErr;
}
}
}
stripeCoupon = await this.stripe.coupons.create(paramsForNewCoupon);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

This replacement flow will keep rotating Stripe coupons after the first immutable change.

When desiredCouponId is already taken, Line 236 creates a replacement coupon with a new Stripe-generated ID, but the next sync starts from Line 218 again instead of reusing the coupon behind the stored stripePromoCodeId. That means every later sync can rediscover the old immutable coupon, deactivate the current promo, and create yet another replacement even if the current promo already points to a matching coupon. Since updateCoupon() now syncs on every save, even a later context/country change can churn promo IDs indefinitely; any failure after deactivation also leaves the database pointing at an inactive promo. Persist the active Stripe coupon ID, or derive/reuse it from coupon.stripePromoCodeId before falling back to desiredCouponId.

Also applies to: 243-287

🧰 Tools
🪛 ESLint

[error] 217-217: Replace 'resource_already_exists' with "resource_already_exists"

(prettier/prettier)


[error] 223-223: Delete ,

(prettier/prettier)


[error] 227-227: Replace coupon.stripePromoCodeId, with ⏎··················coupon.stripePromoCodeId,⏎·················

(prettier/prettier)


[error] 228-228: Insert ··

(prettier/prettier)


[error] 229-229: Replace } with ··}⏎················

(prettier/prettier)


[error] 231-231: Replace 'resource_missing' with "resource_missing"

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/payments/services/coupon.service.ts` around lines 217 - 237, When
desiredCouponId exists but its immutable fields differ, avoid always creating a
new Stripe coupon; first try to derive/reuse the active coupon referenced by
coupon.stripePromoCodeId (retrieve the promotion code via
this.stripe.promotionCodes.retrieve or retrieve the promo code's coupon and
confirm it matches paramsForNewCoupon) and persist that Stripe coupon ID to the
DB (e.g., coupon.stripeCouponId) before deactivating or creating replacements.
Update the flow in the branch that uses
this.stripe.coupons.retrieve(desiredCouponId) /
this.stripe.promotionCodes.update(...) so it: 1) if mismatch, attempt to fetch
the coupon linked to coupon.stripePromoCodeId and use it when it matches, 2)
only deactivate the promo and create paramsForNewCoupon when no active matching
coupon can be found, and 3) persist the chosen active Stripe coupon ID back to
the entity so subsequent updateCoupon() runs reuse it instead of cycling
replacements; apply the same change to the similar block later in the file.

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/payments/services/payment.service.ts (1)

316-318: Consider clearing failureReason when status transitions to success.

The current logic only sets failureReason when the webhook status is failed. If a transaction was previously marked as failed (with a reason) and a subsequent webhook event changes it to success, the old failureReason would remain. This could be misleading in reports.

🛠️ Optional: Clear failure reason on non-failed status updates
          // Update existing transaction
          transactionToUpdate.status = transactionStatus;
          if (failedWebhookReason) {
            transactionToUpdate.failureReason = failedWebhookReason;
+         } else {
+           transactionToUpdate.failureReason = null;
          }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/payments/services/payment.service.ts` around lines 316 - 318, When
updating transaction status in the webhook handler, ensure you clear any stale
failure reason: instead of only setting transactionToUpdate.failureReason =
failedWebhookReason when failedWebhookReason exists, explicitly set
transactionToUpdate.failureReason = null (or undefined) when the incoming status
is not a failure so previous failure text is removed; adjust the logic around
failedWebhookReason assignment in the webhook processing function (the code that
currently uses failedWebhookReason and transactionToUpdate.failureReason) to set
the field on failure and clear it on non-failure transitions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/payments/services/payment.service.ts`:
- Around line 316-318: When updating transaction status in the webhook handler,
ensure you clear any stale failure reason: instead of only setting
transactionToUpdate.failureReason = failedWebhookReason when failedWebhookReason
exists, explicitly set transactionToUpdate.failureReason = null (or undefined)
when the incoming status is not a failure so previous failure text is removed;
adjust the logic around failedWebhookReason assignment in the webhook processing
function (the code that currently uses failedWebhookReason and
transactionToUpdate.failureReason) to set the field on failure and clear it on
non-failure transitions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4e8e8dc9-906e-4ca9-8d1e-4128e9ae9b30

📥 Commits

Reviewing files that changed from the base of the PR and between 720c7c5 and 886c2b4.

📒 Files selected for processing (2)
  • src/payments/providers/stripe/stripe.provider.ts
  • src/payments/services/payment.service.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant