Skip to content

TASK #00000 : Check course pricing and validate while creating intent#735

Merged
vijaykhollam merged 4 commits into
tekdi:aspire-leadersfrom
mahajanmahesh935:PaymentSuccessEmail
May 21, 2026
Merged

TASK #00000 : Check course pricing and validate while creating intent#735
vijaykhollam merged 4 commits into
tekdi:aspire-leadersfrom
mahajanmahesh935:PaymentSuccessEmail

Conversation

@mahajanmahesh935
Copy link
Copy Markdown
Collaborator

@mahajanmahesh935 mahajanmahesh935 commented May 5, 2026

Summary by CodeRabbit

  • New Features
    • Certificate bundle payments now fetch and validate official course pricing from the LMS during payment setup.
    • Discount and final-amount calculations use verified canonical pricing to ensure accurate billing.
    • Multi-course bundles enforce single-currency consistency and reject mismatched amounts to prevent payment errors.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces authoritative price verification for certificate bundles by integrating the LmsClientService into the PaymentService. It adds a new getCoursePricing method to fetch prices from the LMS and a resolveCanonicalOriginalAmount method to ensure the requested payment amount matches the official list price. Feedback includes suggestions to simplify the pricing return logic in the LMS client, parallelize course pricing requests for better performance, and ensure currency fallbacks are consistent with the rest of the system.

Comment thread src/pathways/common/services/lms-client.service.ts
Comment thread src/payments/services/payment.service.ts
Comment thread src/payments/services/payment.service.ts
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Warning

Rate limit exceeded

@mahajanmahesh935 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 29 minutes and 47 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 68926b98-7489-41d9-b957-52635b878ab5

📥 Commits

Reviewing files that changed from the base of the PR and between 254c317 and 4e7ff1c.

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

Walkthrough

A new getCoursePricing method is added to LmsClientService to fetch normalized course pricing from the LMS API. The service is exported from PathwaysModule and imported into PaymentsModule. PaymentService now uses this method to compute and validate a canonical original amount for CERTIFICATE_BUNDLE payments, replacing the client-supplied amount in coupon and discount calculations.

Changes

LMS Course Pricing Integration

Layer / File(s) Summary
LMS Service Enhancement
src/pathways/common/services/lms-client.service.ts
New getCoursePricing(courseId, tenantId, organisationId, academicYearId?) method fetches course pricing from GET /lms-service/v1/courses/:courseId, normalizes response to { courseId, amount, currency }, and returns null on missing config, non-200 status, or errors.
Module Exports & Wiring
src/pathways/pathways.module.ts, src/payments/payments.module.ts
PathwaysModule now exports LmsClientService; PaymentsModule imports PathwaysModule to access LMS pricing functionality.
Payment Service Integration
src/payments/services/payment.service.ts
PaymentService injects ConfigService and LmsClientService. During initiatePayment for CERTIFICATE_BUNDLE, resolveCanonicalOriginalAmount() fetches LMS course pricing, validates distinct courses share a single currency matching the request, verifies requested amount equals LMS total, and replaces dto.amount with this canonical amount for coupon validation, provider checkout, and discount calculations.

Sequence Diagram

sequenceDiagram
    participant Client
    participant PaymentService
    participant LmsClientService
    participant LmsService
    participant PaymentProvider

    Client->>PaymentService: initiatePayment(dto with CERTIFICATE_BUNDLE purpose)
    PaymentService->>PaymentService: Check if CERTIFICATE_BUNDLE
    PaymentService->>LmsClientService: getCoursePricing(courseId) for each course
    LmsClientService->>LmsService: GET /lms-service/v1/courses/:courseId (with headers)
    LmsService-->>LmsClientService: course pricing response
    LmsClientService-->>PaymentService: normalized { courseId, amount, currency }
    PaymentService->>PaymentService: Validate single currency & total matches request
    PaymentService->>PaymentService: Compute canonical original amount from LMS
    PaymentService->>PaymentService: Apply coupon/discount against canonical amount
    PaymentService->>PaymentProvider: Initiate checkout with canonical amount
    PaymentProvider-->>PaymentService: checkout session
    PaymentService-->>Client: payment intent with discountAmount
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • tekdi/user-microservice#637: Both PRs modify src/pathways/common/services/lms-client.service.ts by introducing/expanding the shared LmsClientService.
  • tekdi/user-microservice#634: Both PRs change src/pathways/pathways.module.ts module metadata related to PathwaysModule exports.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding a course pricing check and validation during payment intent creation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/payments/services/payment.service.ts (1)

140-158: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

dto.targets[0].contextId runs before targets are validated.

primaryContextId at line 143 is read before resolveCanonicalOriginalAmount performs the "must include at least one course context" check at lines 84-88. If dto.targets is empty/undefined the request will crash with a TypeError instead of a clean 400. DTO-level validation (@ArrayNotEmpty() on targets) or an early guard in initiatePayment would harden this entry point.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/payments/services/payment.service.ts` around lines 140 - 158,
initiatePayment reads dto.targets[0].contextId before targets are validated,
which can cause a TypeError; fix by enforcing non-empty targets and guarding
early: add `@ArrayNotEmpty`() (and any needed `@ValidateNested`()/@Type) to the
InitiatePaymentDto.targets property so class-validator rejects empty arrays,
and/or add an early check in payment.service.ts inside initiatePayment (before
accessing dto.targets[0]) that verifies dto.targets is an array with length > 0
and throws a BadRequestException with a clear message; after adding the
guard/DTO decorator, keep the existing primaryContextId assignment
(dto.targets[0].contextId) and subsequent calls to
resolveCanonicalOriginalAmount unchanged.
🧹 Nitpick comments (2)
src/payments/payments.module.ts (1)

26-54: ⚖️ Poor tradeoff

Consider extracting LmsClientService into a dedicated module instead of pulling in all of PathwaysModule.

Importing PathwaysModule from PaymentsModule only to access LmsClientService couples the payments domain to pathways' controllers, entities, and transitive imports (InterestsModule, StorageModule, CacheModule, etc.) — none of which PaymentService uses. A small LmsClientModule (providing/exporting only LmsClientService plus ConfigModule) would keep the module graph minimal and avoid accidental circular dependencies as both domains evolve. Not blocking, but worth considering before this surface grows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/payments/payments.module.ts` around lines 26 - 54, PaymentsModule
currently imports PathwaysModule just to consume LmsClientService; extract a
thin LmsClientModule that declares and exports LmsClientService (and imports
ConfigModule as needed), then replace PathwaysModule with LmsClientModule in the
PaymentsModule imports and update providers/constructor injection targets (e.g.,
PaymentService) to consume LmsClientService from the new module; ensure
LmsClientModule exports LmsClientService so DI continues to work and remove
transitive PathwaysModule dependencies from PaymentsModule.
src/pathways/common/services/lms-client.service.ts (1)

542-608: ⚖️ Poor tradeoff

Consider distinguishing transient LMS failures from "no pricing" so callers can retry vs. reject.

Today every failure mode collapses to null (network error, 5xx via validateStatus: status < 500 actually throws, non-200, missing result, etc.), which becomes BadRequestException upstream. A transient LMS outage is then surfaced to the user as "Unable to verify pricing for course …", which masks a 5xx or timeout that would warrant ServiceUnavailableException / retry semantics. Returning a small typed result ({ status: 'ok' | 'not_found' | 'unavailable', ... }) — or at least throwing a distinct error for transient failures — would let PaymentService map each case to the correct HTTP response.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pathways/common/services/lms-client.service.ts` around lines 542 - 608,
The current getCoursePricing collapses all failures to null; change its contract
to distinguish success/not-found/transient by either returning a typed union
(e.g., Promise<{ status: 'ok'|'not_found'|'unavailable'; courseId?: string;
amount?: number; currency?: string }>) or throwing a specific transient error
(e.g., TransientLmsError) so callers (PaymentService) can retry/return 503.
Update getCoursePricing to: treat HTTP 5xx or network/timeout (caught in the
catch block or detected by res.status >= 500) as transient (return status:
'unavailable' or throw TransientLmsError), treat non-200 or missing
result/pricing as 'not_found', and only return status: 'ok' with
courseId/amount/currency when pricing is present; keep using resolvedCourseId
logic and normalize currency as before. Ensure callers are updated to handle the
new union/exception.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pathways/common/services/lms-client.service.ts`:
- Around line 582-600: The current method in lms-client.service that builds the
pricing object returns amount: 0 when pricing or pricing.amount is missing,
which allows paid courses to be treated as free; change its behavior to return
null (i.e., an overall null/unknown pricing result) when pricing is absent or
pricing.amount is undefined/null/NaN or non-numeric, rather than amount: 0.
Specifically, in the function that uses result/pricing and resolvedCourseId,
reject non-numeric values by validating Number(pricing.amount) is finite (not
NaN) before returning an object with amount, otherwise return null so callers
like PaymentService.resolveCanonicalOriginalAmount and amountsMatchClientTotal
fail closed and surface a BadRequestException upstream.

In `@src/payments/services/payment.service.ts`:
- Around line 77-104: The current PaymentService block uses BadRequestException
for server/configuration and LMS/unreachable failures; change the branch that
checks lmsUrl, tenantId, and organisationId to throw
InternalServerErrorException (do not expose raw config key names in the message)
and change the branch after this.lmsClientService.getCoursePricing(...) that
currently throws BadRequestException when pricing is null to throw
ServiceUnavailableException (or rethrow a more specific error from
getCoursePricing and map LMS 5xx/network errors to ServiceUnavailableException
while leaving genuine “no pricing record” as a 4xx only if getCoursePricing can
distinguish them); update messages for lmsUrl/tenantId/organisationId and the
pricing failure to be user-friendly and non-sensitive, and locate these edits
around the unique symbols lmsUrl, tenantId, organisationId, and getCoursePricing
in payment.service.ts.
- Around line 93-113: The current serial loop over uniqueCourseIds calling
this.lmsClientService.getCoursePricing causes per-course latency; change it to
fetch all pricings in parallel by mapping uniqueCourseIds to promises and
awaiting Promise.all, then iterate the resolved pricings to (1) ensure none are
undefined (throw BadRequestException with the specific courseId using the
resolved index), (2) enforce a single pricingCurrency (use the first
non-undefined pricing.currency as the canonical value and compare others,
throwing the existing error message if mismatched), and (3) sum pricing.amount
into total; keep references to uniqueCourseIds,
this.lmsClientService.getCoursePricing, pricingCurrency, and total when applying
the refactor.

---

Outside diff comments:
In `@src/payments/services/payment.service.ts`:
- Around line 140-158: initiatePayment reads dto.targets[0].contextId before
targets are validated, which can cause a TypeError; fix by enforcing non-empty
targets and guarding early: add `@ArrayNotEmpty`() (and any needed
`@ValidateNested`()/@Type) to the InitiatePaymentDto.targets property so
class-validator rejects empty arrays, and/or add an early check in
payment.service.ts inside initiatePayment (before accessing dto.targets[0]) that
verifies dto.targets is an array with length > 0 and throws a
BadRequestException with a clear message; after adding the guard/DTO decorator,
keep the existing primaryContextId assignment (dto.targets[0].contextId) and
subsequent calls to resolveCanonicalOriginalAmount unchanged.

---

Nitpick comments:
In `@src/pathways/common/services/lms-client.service.ts`:
- Around line 542-608: The current getCoursePricing collapses all failures to
null; change its contract to distinguish success/not-found/transient by either
returning a typed union (e.g., Promise<{ status: 'ok'|'not_found'|'unavailable';
courseId?: string; amount?: number; currency?: string }>) or throwing a specific
transient error (e.g., TransientLmsError) so callers (PaymentService) can
retry/return 503. Update getCoursePricing to: treat HTTP 5xx or network/timeout
(caught in the catch block or detected by res.status >= 500) as transient
(return status: 'unavailable' or throw TransientLmsError), treat non-200 or
missing result/pricing as 'not_found', and only return status: 'ok' with
courseId/amount/currency when pricing is present; keep using resolvedCourseId
logic and normalize currency as before. Ensure callers are updated to handle the
new union/exception.

In `@src/payments/payments.module.ts`:
- Around line 26-54: PaymentsModule currently imports PathwaysModule just to
consume LmsClientService; extract a thin LmsClientModule that declares and
exports LmsClientService (and imports ConfigModule as needed), then replace
PathwaysModule with LmsClientModule in the PaymentsModule imports and update
providers/constructor injection targets (e.g., PaymentService) to consume
LmsClientService from the new module; ensure LmsClientModule exports
LmsClientService so DI continues to work and remove transitive PathwaysModule
dependencies from PaymentsModule.
🪄 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: d956b7eb-6e67-4ac2-a837-cc50b238e69a

📥 Commits

Reviewing files that changed from the base of the PR and between 5b016dc and 944512c.

📒 Files selected for processing (4)
  • src/pathways/common/services/lms-client.service.ts
  • src/pathways/pathways.module.ts
  • src/payments/payments.module.ts
  • src/payments/services/payment.service.ts

Comment thread src/pathways/common/services/lms-client.service.ts
Comment thread src/payments/services/payment.service.ts
Comment thread src/payments/services/payment.service.ts
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/payments/services/payment.service.ts (2)

167-175: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bundle coupon eligibility is still based on the first target only.

canonicalOriginalAmount now represents the full bundle total, but validateCoupon() is still fed only dto.targets[0]. That makes promo eligibility depend on target ordering and can let a course-scoped coupon discount the entire bundle. Validate the promo against all distinct bundle contexts, or reject bundle promos until multi-target validation is supported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/payments/services/payment.service.ts` around lines 167 - 175, The code
calls this.couponService.validateCoupon with only the first target
(dto.targets[0]) while canonicalOriginalAmount is the full bundle total, causing
incorrect eligibility; modify the logic in the payment flow to either (A)
validate the promo across all distinct bundle contexts by iterating dto.targets
(deduplicating by contextType+contextId) and calling
couponService.validateCoupon for each context using canonicalOriginalAmount or
per-context amounts as appropriate, aggregating/short-circuiting on failures, or
(B) explicitly reject bundle promos when dto.targets.length > 1 by checking
dto.promoCode and throwing/returning an error; update the block that references
dto.promoCode, dto.targets, canonicalOriginalAmount and the call to
couponService.validateCoupon to implement one of these fixes so eligibility is
not determined solely by the first target.

145-156: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Check every bundle course before creating the intent.

This guard only looks at dto.targets[0].contextId. With the new multi-course CERTIFICATE_BUNDLE flow, a user can still start a payment for a bundle where a later contextId is already paid, which risks duplicate charges/unlocks for part of the bundle.

🛠️ Possible fix
-    const primaryContextId = dto.targets[0].contextId;
-    const existingPaid =
-      await this.paymentIntentService.findPaidByUserIdAndContextId(
-        dto.userId,
-        primaryContextId,
-      );
+    const contextIds = [...new Set(dto.targets.map((t) => t.contextId))];
+    const existingPaid = (
+      await Promise.all(
+        contextIds.map((contextId) =>
+          this.paymentIntentService.findPaidByUserIdAndContextId(
+            dto.userId,
+            contextId,
+          ),
+        ),
+      )
+    ).find(Boolean);
     if (existingPaid) {
       throw new ConflictException({
         message: 'Already paid for this context.',
         alreadyPaid: true,
         paymentIntentId: existingPaid.id,
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/payments/services/payment.service.ts` around lines 145 - 156, The current
guard only checks dto.targets[0].contextId and misses paid contexts in other
targets; update the check in the create-intent flow (the block calling
paymentIntentService.findPaidByUserIdAndContextId) to iterate over all
dto.targets, call paymentIntentService.findPaidByUserIdAndContextId(dto.userId,
target.contextId) for each target, and if any call returns a paid intent throw
the same ConflictException (include message, alreadyPaid: true and
paymentIntentId: existingPaid.id) so the request is rejected when any bundle
course/context is already paid.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/payments/services/payment.service.ts`:
- Around line 167-175: The code calls this.couponService.validateCoupon with
only the first target (dto.targets[0]) while canonicalOriginalAmount is the full
bundle total, causing incorrect eligibility; modify the logic in the payment
flow to either (A) validate the promo across all distinct bundle contexts by
iterating dto.targets (deduplicating by contextType+contextId) and calling
couponService.validateCoupon for each context using canonicalOriginalAmount or
per-context amounts as appropriate, aggregating/short-circuiting on failures, or
(B) explicitly reject bundle promos when dto.targets.length > 1 by checking
dto.promoCode and throwing/returning an error; update the block that references
dto.promoCode, dto.targets, canonicalOriginalAmount and the call to
couponService.validateCoupon to implement one of these fixes so eligibility is
not determined solely by the first target.
- Around line 145-156: The current guard only checks dto.targets[0].contextId
and misses paid contexts in other targets; update the check in the create-intent
flow (the block calling paymentIntentService.findPaidByUserIdAndContextId) to
iterate over all dto.targets, call
paymentIntentService.findPaidByUserIdAndContextId(dto.userId, target.contextId)
for each target, and if any call returns a paid intent throw the same
ConflictException (include message, alreadyPaid: true and paymentIntentId:
existingPaid.id) so the request is rejected when any bundle course/context is
already paid.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8f021f92-9366-4fd8-8146-735abb6f19b4

📥 Commits

Reviewing files that changed from the base of the PR and between 944512c and 254c317.

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

@sonarqubecloud
Copy link
Copy Markdown

@vijaykhollam vijaykhollam merged commit 6ce24f6 into tekdi:aspire-leaders May 21, 2026
3 checks passed
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.

2 participants