Skip to content

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

Open
mahajanmahesh935 wants to merge 1 commit into
tekdi:aspire-leadersfrom
mahajanmahesh935:PaymentSuccessEmail
Open

TASK #00000 : Check course pricing and validate while creating intent#735
mahajanmahesh935 wants to merge 1 commit into
tekdi:aspire-leadersfrom
mahajanmahesh935:PaymentSuccessEmail

Conversation

@mahajanmahesh935
Copy link
Copy Markdown
Collaborator

@mahajanmahesh935 mahajanmahesh935 commented May 5, 2026

Summary by CodeRabbit

Release Notes

  • New Features
    • Certificate bundle payment processing now automatically retrieves and validates course pricing information during payment initialization, with support for multi-course bundles.
    • Discount calculations enhanced through improved validation against verified course pricing data from the course management system.
    • Added multi-currency validation to ensure pricing consistency across certificate bundles with multiple courses.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 5, 2026

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 on lines +582 to +600
const pricing = result.pricing;
const resolvedCourseId = result.courseId || courseId;
if (!pricing || pricing.amount === undefined || pricing.amount === null) {
return {
courseId: resolvedCourseId,
amount: 0,
currency: String(pricing?.currency || 'USD')
.toUpperCase()
.slice(0, 3),
};
}

return {
courseId: resolvedCourseId,
amount: Number(pricing.amount),
currency: String(pricing.currency || 'USD')
.toUpperCase()
.slice(0, 3),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The pricing validation and return logic can be simplified to improve readability and reduce duplication of the currency normalization logic.

Suggested change
const pricing = result.pricing;
const resolvedCourseId = result.courseId || courseId;
if (!pricing || pricing.amount === undefined || pricing.amount === null) {
return {
courseId: resolvedCourseId,
amount: 0,
currency: String(pricing?.currency || 'USD')
.toUpperCase()
.slice(0, 3),
};
}
return {
courseId: resolvedCourseId,
amount: Number(pricing.amount),
currency: String(pricing.currency || 'USD')
.toUpperCase()
.slice(0, 3),
};
const pricing = result.pricing;
const resolvedCourseId = result.courseId || courseId;
const currency = String(pricing?.currency || 'USD')
.toUpperCase()
.slice(0, 3);
return {
courseId: resolvedCourseId,
amount: pricing?.amount != null ? Number(pricing.amount) : 0,
currency,
};

Comment on lines +93 to +113
for (const courseId of uniqueCourseIds) {
const pricing = await this.lmsClientService.getCoursePricing(
courseId,
tenantId,
organisationId,
academicYearId,
);
if (!pricing) {
throw new BadRequestException(
`Unable to verify pricing for course ${courseId}.`,
);
}
if (pricingCurrency === undefined) {
pricingCurrency = pricing.currency;
} else if (pricing.currency !== pricingCurrency) {
throw new BadRequestException(
'Cannot combine courses with different pricing currencies in a single payment.',
);
}
total += pricing.amount;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The course pricing is being fetched sequentially in a loop. This can lead to significant performance degradation if the bundle contains multiple courses. It is recommended to fetch the pricing for all courses in parallel using Promise.all.

    const pricingResults = await Promise.all(
      uniqueCourseIds.map((courseId) =>
        this.lmsClientService.getCoursePricing(
          courseId,
          tenantId,
          organisationId,
          academicYearId,
        ),
      ),
    );

    for (let i = 0; i < uniqueCourseIds.length; i++) {
      const pricing = pricingResults[i];
      const courseId = uniqueCourseIds[i];
      if (!pricing) {
        throw new BadRequestException(
          `Unable to verify pricing for course ${courseId}.`,
        );
      }
      if (pricingCurrency === undefined) {
        pricingCurrency = pricing.currency;
      } else if (pricing.currency !== pricingCurrency) {
        throw new BadRequestException(
          'Cannot combine courses with different pricing currencies in a single payment.',
        );
      }
      total += pricing.amount;
    }

Comment on lines +117 to +118
const requestedCurrency = (dto.currency || 'USD').toUpperCase().slice(0, 3);
const canonicalCurrency = (pricingCurrency || 'USD').toUpperCase().slice(0, 3);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The currency handling here has two issues:

  1. The fallback 'USD' is inconsistent with the system default 'INR' defined in the PaymentIntent entity and InitiatePaymentDto.
  2. The normalization of pricingCurrency is redundant as it is already normalized in getCoursePricing.
Suggested change
const requestedCurrency = (dto.currency || 'USD').toUpperCase().slice(0, 3);
const canonicalCurrency = (pricingCurrency || 'USD').toUpperCase().slice(0, 3);
const requestedCurrency = (dto.currency || 'INR').toUpperCase().slice(0, 3);
const canonicalCurrency = pricingCurrency;

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

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
    LmsService-->>LmsClientService: course pricing response
    LmsClientService-->>PaymentService: normalized { courseId, amount, currency }
    PaymentService->>PaymentService: Validate: single currency, amount 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

🚥 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 directly describes the main change: adding course pricing validation during payment intent creation, which aligns with the primary modifications across PaymentService, LmsClientService, and module exports.
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 on lines +582 to +600
const pricing = result.pricing;
const resolvedCourseId = result.courseId || courseId;
if (!pricing || pricing.amount === undefined || pricing.amount === null) {
return {
courseId: resolvedCourseId,
amount: 0,
currency: String(pricing?.currency || 'USD')
.toUpperCase()
.slice(0, 3),
};
}

return {
courseId: resolvedCourseId,
amount: Number(pricing.amount),
currency: String(pricing.currency || 'USD')
.toUpperCase()
.slice(0, 3),
};
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 | ⚡ Quick win

Silent amount: 0 fallback can let paid courses through pricing validation.

When the LMS response is missing pricing or pricing.amount, this method returns { amount: 0, currency: <derived> } instead of null. In PaymentService.resolveCanonicalOriginalAmount, that 0 will be summed into the canonical total, so a misconfigured/incomplete LMS record for a paid course will silently let a dto.amount of 0 (or a smaller-than-list total) pass amountsMatchClientTotal and proceed to checkout. Since the new method's stated purpose is "authoritative list price for checkout validation", absence of pricing.amount should be treated as "unknown" rather than "free" — return null (or throw) so the caller fails closed with BadRequestException. Also note Number(pricing.amount) can produce NaN for non-numeric values; that should be rejected too.

🛡️ Proposed fix
-      const pricing = result.pricing;
-      const resolvedCourseId = result.courseId || courseId;
-      if (!pricing || pricing.amount === undefined || pricing.amount === null) {
-        return {
-          courseId: resolvedCourseId,
-          amount: 0,
-          currency: String(pricing?.currency || 'USD')
-            .toUpperCase()
-            .slice(0, 3),
-        };
-      }
-
-      return {
-        courseId: resolvedCourseId,
-        amount: Number(pricing.amount),
-        currency: String(pricing.currency || 'USD')
-          .toUpperCase()
-          .slice(0, 3),
-      };
+      const pricing = result.pricing;
+      const resolvedCourseId = result.courseId || courseId;
+      if (pricing?.amount == null) {
+        this.logger.warn(
+          `LMS course ${courseId} has no pricing.amount; refusing to validate checkout total.`,
+        );
+        return null;
+      }
+      const amount = Number(pricing.amount);
+      if (!Number.isFinite(amount) || amount < 0) {
+        this.logger.warn(
+          `LMS course ${courseId} returned invalid pricing.amount: ${pricing.amount}`,
+        );
+        return null;
+      }
+      return {
+        courseId: resolvedCourseId,
+        amount,
+        currency: String(pricing.currency || 'USD').toUpperCase().slice(0, 3),
+      };
🧰 Tools
🪛 ESLint

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

(prettier/prettier)


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

(prettier/prettier)

🪛 GitHub Check: SonarCloud Code Analysis

[warning] 584-584: Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=tekdi_user-microservice&issues=AZ33a2epdMPnwoeOq0yZ&open=AZ33a2epdMPnwoeOq0yZ&pullRequest=735

🤖 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 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.

Comment on lines +77 to +104
if (!lmsUrl || !tenantId || !organisationId) {
throw new BadRequestException(
'Course pricing cannot be verified: configure LMS_SERVICE_URL, DEFAULT_TENANT_ID, and DEFAULT_ORGANISATION_ID.',
);
}

const uniqueCourseIds = [...new Set(dto.targets.map((t) => t.contextId))];
if (uniqueCourseIds.length === 0) {
throw new BadRequestException(
'Payment targets must include at least one course context.',
);
}

let total = 0;
let pricingCurrency: string | undefined;

for (const courseId of uniqueCourseIds) {
const pricing = await this.lmsClientService.getCoursePricing(
courseId,
tenantId,
organisationId,
academicYearId,
);
if (!pricing) {
throw new BadRequestException(
`Unable to verify pricing for course ${courseId}.`,
);
}
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 | 🟡 Minor | ⚡ Quick win

Misuse of BadRequestException for server-side / transient failures.

Two of these branches are not user input problems:

  • Lines 77-81: missing LMS_SERVICE_URL / DEFAULT_TENANT_ID / DEFAULT_ORGANISATION_ID is a server misconfiguration (HTTP 500/Service Unavailable), not a 400.
  • Lines 100-104: getCoursePricing returning null collapses LMS 5xx, network timeouts, and "no pricing record" into a 400 — masking outages as client errors and bypassing typical retry/alerting on 5xx.

Use InternalServerErrorException for the misconfiguration path and ServiceUnavailableException for the LMS-unreachable path (after getCoursePricing distinguishes them — see related comment in lms-client.service.ts). This also avoids leaking config-key names in user-facing error bodies.

As per coding guidelines: "The code adheres to best practices associated with nestjs framework."

🧰 Tools
🪛 ESLint

[error] 79-79: Replace 'Course·pricing·cannot·be·verified:·configure·LMS_SERVICE_URL,·DEFAULT_TENANT_ID,·and·DEFAULT_ORGANISATION_ID.', with "Course·pricing·cannot·be·verified:·configure·LMS_SERVICE_URL,·DEFAULT_TENANT_ID,·and·DEFAULT_ORGANISATION_ID."

(prettier/prettier)


[error] 86-86: Replace 'Payment·targets·must·include·at·least·one·course·context.', with "Payment·targets·must·include·at·least·one·course·context."

(prettier/prettier)


[error] 98-98: Delete ,

(prettier/prettier)


[error] 102-102: Delete ,

(prettier/prettier)

🤖 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 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.

Comment on lines +93 to +113
for (const courseId of uniqueCourseIds) {
const pricing = await this.lmsClientService.getCoursePricing(
courseId,
tenantId,
organisationId,
academicYearId,
);
if (!pricing) {
throw new BadRequestException(
`Unable to verify pricing for course ${courseId}.`,
);
}
if (pricingCurrency === undefined) {
pricingCurrency = pricing.currency;
} else if (pricing.currency !== pricingCurrency) {
throw new BadRequestException(
'Cannot combine courses with different pricing currencies in a single payment.',
);
}
total += pricing.amount;
}
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 | ⚡ Quick win

Sequential getCoursePricing calls add per-course latency to checkout initiation.

For a bundle of N distinct courses, this awaits each LMS call serially with a 15s timeout each (getCoursePricing in lms-client.service.ts). A 5-course bundle on a slow LMS becomes 5×latency on the user's checkout request. Run them in parallel with Promise.all and aggregate afterwards — currency uniformity and total can be validated on the resolved array.

⚡ Proposed parallelization
-    let total = 0;
-    let pricingCurrency: string | undefined;
-
-    for (const courseId of uniqueCourseIds) {
-      const pricing = await this.lmsClientService.getCoursePricing(
-        courseId,
-        tenantId,
-        organisationId,
-        academicYearId,
-      );
-      if (!pricing) {
-        throw new BadRequestException(
-          `Unable to verify pricing for course ${courseId}.`,
-        );
-      }
-      if (pricingCurrency === undefined) {
-        pricingCurrency = pricing.currency;
-      } else if (pricing.currency !== pricingCurrency) {
-        throw new BadRequestException(
-          'Cannot combine courses with different pricing currencies in a single payment.',
-        );
-      }
-      total += pricing.amount;
-    }
+    const pricings = await Promise.all(
+      uniqueCourseIds.map((courseId) =>
+        this.lmsClientService
+          .getCoursePricing(courseId, tenantId, organisationId, academicYearId)
+          .then((p) => ({ courseId, pricing: p })),
+      ),
+    );
+
+    let total = 0;
+    let pricingCurrency: string | undefined;
+    for (const { courseId, pricing } of pricings) {
+      if (!pricing) {
+        throw new BadRequestException(
+          `Unable to verify pricing for course ${courseId}.`,
+        );
+      }
+      if (pricingCurrency === undefined) {
+        pricingCurrency = pricing.currency;
+      } else if (pricing.currency !== pricingCurrency) {
+        throw new BadRequestException(
+          'Cannot combine courses with different pricing currencies in a single payment.',
+        );
+      }
+      total += pricing.amount;
+    }

As per coding guidelines: "The code adheres to best practices recommended for performance."

🧰 Tools
🪛 ESLint

[error] 98-98: Delete ,

(prettier/prettier)


[error] 102-102: Delete ,

(prettier/prettier)


[error] 109-109: Replace 'Cannot·combine·courses·with·different·pricing·currencies·in·a·single·payment.', with "Cannot·combine·courses·with·different·pricing·currencies·in·a·single·payment."

(prettier/prettier)

🤖 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 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.

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