TASK #00000 : Check course pricing and validate while creating intent#735
TASK #00000 : Check course pricing and validate while creating intent#735mahajanmahesh935 wants to merge 1 commit into
Conversation
|
There was a problem hiding this comment.
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.
| 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), | ||
| }; |
There was a problem hiding this comment.
The pricing validation and return logic can be simplified to improve readability and reduce duplication of the currency normalization logic.
| 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, | |
| }; |
| 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; | ||
| } |
There was a problem hiding this comment.
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;
}| const requestedCurrency = (dto.currency || 'USD').toUpperCase().slice(0, 3); | ||
| const canonicalCurrency = (pricingCurrency || 'USD').toUpperCase().slice(0, 3); |
There was a problem hiding this comment.
The currency handling here has two issues:
- The fallback 'USD' is inconsistent with the system default 'INR' defined in the
PaymentIntententity andInitiatePaymentDto. - The normalization of
pricingCurrencyis redundant as it is already normalized ingetCoursePricing.
| 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; |
WalkthroughA new ChangesLMS Course Pricing Integration
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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].contextIdruns before targets are validated.
primaryContextIdat line 143 is read beforeresolveCanonicalOriginalAmountperforms the "must include at least one course context" check at lines 84-88. Ifdto.targetsis empty/undefined the request will crash with aTypeErrorinstead of a clean 400. DTO-level validation (@ArrayNotEmpty()ontargets) or an early guard ininitiatePaymentwould 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 tradeoffConsider extracting
LmsClientServiceinto a dedicated module instead of pulling in all ofPathwaysModule.Importing
PathwaysModulefromPaymentsModuleonly to accessLmsClientServicecouples the payments domain to pathways' controllers, entities, and transitive imports (InterestsModule,StorageModule,CacheModule, etc.) — none of whichPaymentServiceuses. A smallLmsClientModule(providing/exporting onlyLmsClientServiceplusConfigModule) 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 tradeoffConsider distinguishing transient LMS failures from "no pricing" so callers can retry vs. reject.
Today every failure mode collapses to
null(network error, 5xx viavalidateStatus: status < 500actually throws, non-200, missingresult, etc.), which becomesBadRequestExceptionupstream. 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 warrantServiceUnavailableException/ retry semantics. Returning a small typed result ({ status: 'ok' | 'not_found' | 'unavailable', ... }) — or at least throwing a distinct error for transient failures — would letPaymentServicemap 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
📒 Files selected for processing (4)
src/pathways/common/services/lms-client.service.tssrc/pathways/pathways.module.tssrc/payments/payments.module.tssrc/payments/services/payment.service.ts
| 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), | ||
| }; |
There was a problem hiding this comment.
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.
🤖 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.
| 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}.`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
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_IDis a server misconfiguration (HTTP 500/Service Unavailable), not a 400. - Lines 100-104:
getCoursePricingreturningnullcollapses 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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.



Summary by CodeRabbit
Release Notes