Feat/medusajs integration for checkout process#679
Conversation
- Introduced cart-related modules to Medusa.js integration with support for create, update, and delete operations. - Included mappings and services for handling carts across integrations. - Updated relevant configurations, docs, and seed data.
- Added models, services, and mappers for customers, payments, and checkout. - Enhanced carts and orders to support additional fields like email and payment session. - Integrated mocked module functionality for new services. - Refined Medusa.js mappings for customer addresses and shipping options.
- Renamed methods, parameters, and models for clarity and consistency (e.g., `setupAddresses` to `setAddresses`). - Centralized support for guest email handling using `email` field across carts, orders, and checkout processes. - Removed redundant calls and optimized order completion workflow.
- Added locale-based grouping for payment providers. - Introduced utility to retrieve localized payment method display info. - Updated cart and checkout services to support locale-based operations. - Enhanced cart item handling with improved product mapping and variant support. - Addressed stricter authentication for customer cart access and actions.
…oducts, customers, and orders
…d field mappings - Added documentation for Carts, Customers, and Checkout services, including API methods and data models. - Updated Products module to reference associated entities (e.g., Carts and Orders). - Enhanced Tickets API docs with clarified field mapping and integration-specific requirements. - Improved docs with consistent formatting.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds Carts, Customers, Payments, and Checkout domains across framework, configs, Medusa.js and mocked integrations, including models, DTOs, controllers, abstract service contracts, concrete Medusa + mocked implementations, mappers, tests, app wiring, and extensive documentation updates. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant API as Harmonization API
participant Checkout as Checkout.Service
participant Carts as Carts.Service
participant Payments as Payments.Service
participant Customers as Customers.Service
rect rgba(200,230,255,0.5)
Client->>API: POST /checkout/:cartId/addresses (body, auth)
API->>Checkout: setAddresses(params, body, auth)
Checkout->>Carts: updateCartAddresses(params, body, auth)
Carts-->>Checkout: updated Cart
Checkout->>Payments: (optional) create payment session / update cart metadata
Checkout->>Customers: (optional) resolve saved addresses by id
Checkout-->>API: CheckoutSummary / Cart
API-->>Client: 200 OK
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 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: 16
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
packages/integrations/mocked/prisma/seed.ts (1)
9-18:⚠️ Potential issue | 🟡 MinorStale comment references old customer ID.
Line 9 still references
cust-001for Acme Corporation, but the actual value on line 18 is nowcus_01KH3J08TY40PYGVEG3A04CP8R. Update the comment to stay consistent with the code.Also,
cust-002andcust-003(lines 25, 32) still use the old ID format. If these haven't been migrated intentionally, that's fine — but if the plan is to migrate all customer IDs, this is incomplete.Suggested comment fix
// Customer permissions defined in customers.mapper.ts: - // - cust-001 (Acme Corporation): ADMIN_PERMISSIONS (full access) + // - cus_01KH3J08TY40PYGVEG3A04CP8R (Acme Corporation): ADMIN_PERMISSIONS (full access) // - cust-002 (Tech Solutions Inc): USER_PERMISSIONS (view + pay) // - cust-003 (Digital Services GmbH): READONLY_PERMISSIONS (view only)packages/integrations/mocked/src/modules/auth/auth.service.ts (1)
56-56:⚠️ Potential issue | 🟡 MinorInconsistent optional chaining — potential NPE on
decodedToken.permissions.Line 48 correctly uses
decodedToken?.rolesas a fallback, but line 56 usesdecodedToken.permissions(no?.). IfdecodedTokenisundefined(e.g., malformed token), this will throw aTypeError.Suggested fix
- return decodedToken?.customer?.permissions || decodedToken.permissions || {}; + return decodedToken?.customer?.permissions || decodedToken?.permissions || {};packages/integrations/mocked/src/modules/users/customers.mapper.ts (1)
34-51:⚠️ Potential issue | 🟡 MinorInconsistent customer ID format across mock customers.
MOCK_CUSTOMER_1now uses a Medusa-style ID (cus_01KH3J08TY40PYGVEG3A04CP8R), whileMOCK_CUSTOMER_2andMOCK_CUSTOMER_3retain the oldcust-002/cust-003format. If downstream code or tests depend on a consistent format, this could cause issues. Consider aligning all mock IDs to the same convention.Also applies to: 53-70, 72-89
packages/integrations/medusajs/src/modules/orders/orders.mapper.ts (1)
41-52:⚠️ Potential issue | 🟠 MajorUse
item.product_idforproductIdinstead ofitem.variant_id.Line 44 maps
productIdfromitem.variant_id, but it should useitem.product_id(available at line 62 inmapProduct). The field name indicates a product identifier, not a variant identifier, and this inconsistency causes the OrderItem to reference the variant ID when it should reference the product ID.Current code
const mapOrderItem = (item: HttpTypes.StoreOrderLineItem, currency: string): Orders.Model.OrderItem => { return { id: item.id, productId: item.variant_id || '', // ← should be item.product_id quantity: item.quantity, price: mapPrice(item.unit_price, currency) as Models.Price.Price, total: mapPrice(item.total, currency), subtotal: mapPrice(item.subtotal, currency), currency: currency as Models.Price.Currency, product: mapProduct(item.unit_price, currency, item) as Products.Model.Product, }; };packages/integrations/medusajs/src/modules/orders/orders.service.ts (2)
29-31:⚠️ Potential issue | 🟡 MinorDuplicate
+tax_totalinadditionalOrderListFields.The field string contains
+tax_totaltwice. This won't cause an error but is redundant.🔧 Proposed fix
private readonly additionalOrderListFields = - '+total,+subtotal,+tax_total,+discount_total,+shipping_total,+shipping_subtotal,+tax_total,+items.product.*'; + '+total,+subtotal,+tax_total,+discount_total,+shipping_total,+shipping_subtotal,+items.product.*';
37-37:⚠️ Potential issue | 🟡 MinorRemove unused
authServicedependency.The
Auth.Serviceis injected on line 37 but never referenced in the file. After migrating to the Medusa Store API, which automatically scopes orders to the authenticated customer, this dependency is no longer needed and should be removed from the constructor.
🤖 Fix all issues with AI agents
In `@packages/framework/src/modules/carts/carts.service.ts`:
- Line 3: Replace the circular barrel import "import * as Carts from './';" with
direct imports for the types used: "import * as Model from './carts.model';" and
"import * as Request from './carts.request';", then update all occurrences of
Carts.Model.* to Model.* and Carts.Request.* to Request.* in this file (e.g.,
references in type annotations, function params, and return types); this removes
the circular dependency caused by the index re-export of CartService (exported
as Service) while keeping all type usages intact.
In `@packages/framework/src/modules/checkout/checkout.controller.ts`:
- Around line 9-12: CheckoutController methods are missing the project's auth
decorator; add `@Auth.Decorators.Roles`({ roles: [] }) to every public endpoint
method in the CheckoutController class (e.g., the methods handling "place-order"
and "complete" and any other checkout operations) so they enforce authentication
like UserController; if guest checkout is allowed, instead apply a distinct auth
pattern (e.g., a separate decorator or conditional guard) only to the
guest-specific methods and keep secure endpoints using Roles on the
corresponding methods.
In `@packages/integrations/medusajs/src/integration.ts`:
- Around line 42-46: The payments integration is missing the Auth.Module in its
imports which PaymentsService requires for validating SSO tokens; update the
payments entry (symbol: payments) so its imports include Auth.Module alongside
MedusaJsModule (i.e., change imports: [MedusaJsModule] to imports:
[MedusaJsModule, Auth.Module]) to match the other integrations and satisfy
PaymentsService's auth dependency.
In `@packages/integrations/medusajs/src/modules/carts/carts.mapper.ts`:
- Around line 15-19: The mapper mapCart is casting Medusa's lowercase
cart.currency_code directly to Models.Price.Currency which allows invalid
lowercase values; change mapCart to normalize currency by calling
cart.currency_code.toUpperCase() and then cast to Models.Price.Currency (i.e.,
const currency = cart.currency_code.toUpperCase() as Models.Price.Currency) so
all price objects use the uppercase enum, and update the test in
carts.service.spec.ts to expect 'EUR' instead of 'eur'; mirror the pattern used
in checkout.mapper's currency handling for consistency.
In `@packages/integrations/medusajs/src/modules/carts/carts.service.ts`:
- Around line 187-209: The authenticated branch for customerId calls
createCartAndAddItem without the authorization token, so Medusa can't associate
the cart with the customer; update the customerId branch to pass the
authorization argument (same position as in the guest call) to
createCartAndAddItem (refer to the createCartAndAddItem invocation in the
customerId conditional) so both authenticated and guest flows forward the auth
token correctly.
- Around line 252-270: The applyPromotion method currently calls
this.sdk.store.cart.update with promo_codes (in applyPromotion) which overwrites
existing codes; change it to call the dedicated store cart promotions endpoint
to add a code (POST /store/carts/{id}/promotions) instead of cart.update so it
appends rather than replaces; update the call in applyPromotion to use the SDK
method that executes the store cart promotions POST with params.cartId and
payload containing the code, passing
this.medusaJsService.getStoreApiHeaders(authorization), and preserve the same
response mapping (mapCart) and error handling (handleHttpError).
In `@packages/integrations/medusajs/src/modules/checkout/checkout.service.ts`:
- Around line 96-101: The object in checkout.service.ts currently uses hardcoded
returnUrl/cancelUrl which won't work in production; update the code that builds
the checkout payload (the object with cartId, providerId, metadata) to read
returnUrl and cancelUrl from the incoming request data (e.g., data.returnUrl,
data.cancelUrl) or from a config/provider (env/config service) if absent, and
validate they exist (throw or return a clear error) before sending to the
checkout provider; also mirror the same change in the mocked checkout service so
both implementations accept and propagate dynamic URLs rather than the
example.com placeholders.
- Around line 244-246: The ternary assigning const headers in
checkout.service.ts is redundant (both branches call
this.medusaJsService.getStoreApiHeaders(authorization)); remove the dead ternary
and call getStoreApiHeaders once; either use
this.medusaJsService.getStoreApiHeaders(authorization) if passing undefined is
acceptable, or call this.medusaJsService.getStoreApiHeaders() in the else-case
if you intend unauthenticated headers—update the single assignment accordingly
and keep getStoreApiHeaders, authorization references intact.
In `@packages/integrations/medusajs/src/modules/medusajs/medusajs.service.ts`:
- Around line 68-77: The Medusa client is instantiated with a hardcoded debug:
true in the Medusa constructor (this._sdk) which was left from development;
replace the hardcoded value with the intended conditional (use this.logLevel ===
'debug' or equivalent configuration flag) so debug is enabled only in debug
mode. Locate the Medusa initialization in medusajs.service.ts (the this._sdk =
new Medusa({...}) block) and change the debug property to a boolean expression
that checks the service's log level or environment setting (e.g., this.logLevel
=== 'debug') so production does not get verbose logging.
- Around line 126-132: getStoreApiHeaders currently omits the required Medusa
header causing raw HTTP calls to fail; update getStoreApiHeaders to always
include the Medusa publishable key by adding 'x-publishable-api-key' with
this.getPublishableKey() to the returned headers, and preserve the optional
Authorization header if provided (used by payments.service.ts calls). Locate
getStoreApiHeaders in medusajs.service.ts and change its headers initialization
to include the publishable key before returning.
In
`@packages/integrations/medusajs/src/modules/payments/payments.service.spec.ts`:
- Around line 213-218: Update the production code to stop calling .toPromise()
on HttpClient Observables: replace patterns like
from(this.httpClient.post(...).toPromise()).pipe(...) with
this.httpClient.post(...).pipe(...) (or, if a Promise is truly required, wrap
lastValueFrom(this.httpClient.post(...)) instead). Then update the tests in
payments.service.spec.ts where mockHttpService.post.mockReturnValue(...) returns
an object with toPromise — replace those mocks (the one returning { toPromise:
() => Promise.resolve({ data: { payment_session: ... } }) } and the other
similar instances) to return proper Observables using rxjs (e.g., of({ data: {
payment_session: ... } })) so they match the new Observable-based production
calls. Ensure all four test mocks referenced are changed consistently.
In `@packages/integrations/medusajs/src/modules/payments/payments.service.ts`:
- Around line 192-208: The cancelSession method is using the deprecated
.toPromise() on the Observable returned by this.httpClient.delete; replace the
.toPromise() usage with firstValueFrom to await the HTTP Observable instead.
Update the code in cancelSession (the call that wraps
this.httpClient.delete(...).toPromise()) to use
firstValueFrom(this.httpClient.delete(...)) and ensure firstValueFrom is
imported from 'rxjs', leaving the existing map/ catchError logic (including
NotFoundException and handleHttpError) unchanged so behavior and error handling
for medusaJsService.getBaseUrl()/getStoreApiHeaders remain intact.
- Around line 154-180: The current updateSession implementation incorrectly
converts the HTTP observable to a promise with .toPromise() and then wraps it
with from(), and it also injects an empty string as cartId into
mapPaymentSession; fix by removing the .toPromise() call and the outer from(...)
wrapper and instead operate directly on the Observable returned by
this.httpClient.post(...).pipe(...), keeping the existing map and catchError
operators on that observable (importing no new RxJS conversion helpers). In the
map operator replace the hard-coded const cartId = '' with a real extraction
from the response (e.g., response.data.payment_session.cart_id) or pass
undefined/null if absent and update mapPaymentSession usage accordingly (ensure
mapPaymentSession handles nullable cartId or accept cartId as an input parameter
instead). Reference symbols: httpClient.post, mapPaymentSession, updateSession
(the method containing this code), and the map/catchError operators.
In `@packages/integrations/medusajs/src/modules/products/products.mapper.ts`:
- Around line 238-271: mapRelatedProducts currently sets id to targetProduct.id
(a variant ID) which causes inconsistency with mapProduct (which uses
product?.id); change the id field in the mapping returned by mapRelatedProducts
to use the product-level id (use product?.id || targetProduct.product_id || ''
to cover shapes) so the Products.Model.Product.id is always the product ID;
update any other fields that might assume id is a variant to continue using
targetProduct.id if needed but keep the primary id as the product id.
In `@packages/integrations/medusajs/src/modules/products/products.service.ts`:
- Around line 78-101: The getProduct flow throws plain Error for missing
variants/variant (currently in the map() callback around the checks "No variants
found for product" and "Variant ... not found for product"), which causes
handleHttpError to convert them to 500s; replace those throws with Nest's
NotFoundException (imported from `@nestjs/common`) so missing-product/variant
cases return 404, or alternatively perform the variant existence validation
before the RxJS map() stage and emit a NotFoundException there; ensure the
change references the mapProduct/handleHttpError logic and keeps the error type
as NotFoundException so the HTTP pipeline returns 404 instead of 500.
In `@packages/integrations/mocked/src/modules/checkout/checkout.service.ts`:
- Around line 13-19: The CheckoutService currently declares unused dependencies
authService and customersService and incorrectly uses "implements
Checkout.Service" while Checkout.Service is an abstract class; remove the unused
constructor params/fields (authService, customersService), change the class
declaration to "extends Checkout.Service", and add a super(...) call in the
constructor passing the dependencies expected by the abstract base (e.g., pass
the required services such as cartsService and paymentsService per
Checkout.Service's constructor signature) so the subclass properly delegates
initialization to the base class.
🟡 Minor comments (13)
packages/integrations/medusajs/src/modules/products/products.mapper.ts-183-234 (1)
183-234:⚠️ Potential issue | 🟡 Minor
variantIddefaults to empty string when no variants exist.Line 207: if a product has no variants,
variantIdwill be''. If downstream code usesvariantIdto construct API calls (e.g., add-to-cart, pricing lookups), an empty string could cause silent failures or invalid requests. Consider whether products without variants should be filtered out of the list instead.packages/integrations/medusajs/src/modules/products/products.service.ts-56-71 (1)
56-71:⚠️ Potential issue | 🟡 MinorPush category filtering to the Store API server-side to maintain pagination accuracy.
getProductListcurrently filters bycategoryFilterclient-side (inmapProductslines ~191-193), which causes the returned product count to be less thanlimitand setstotaltoproducts.lengthinstead of the actual total for that category. The Medusa Store API supportscategory_idfiltering, so the filter should be passed as a param tothis.sdk.store.product.list()instead of applying it after the response. This ensures the API respects pagination bounds and returns the correct count metadata.packages/integrations/mocked/src/modules/checkout/checkout.mapper.ts-92-171 (1)
92-171:⚠️ Potential issue | 🟡 MinorAll locales use USD currency — intentional for mocks?
The German (
de) and Polish (pl) shipping options all use'USD'as the currency. If this mocked data is intended to test locale-specific behavior, consider using'EUR'or'PLN'for the respective locales. If the mock is purely for structural testing and currency is irrelevant, this is fine — but a brief comment would clarify intent.packages/integrations/medusajs/src/modules/customers/customers.service.ts-118-135 (1)
118-135:⚠️ Potential issue | 🟡 MinorFragile assumption: newly created address is the last in the array.
Line 122 assumes the newly created address is
addresses[addresses.length - 1]. If Medusa ever changes the ordering (e.g., sorts alphabetically, byis_default, or byid), this will silently return the wrong address. Consider matching by a known field (e.g., comparing the mapped address fields) or using the response more precisely if Medusa returns the created address ID.packages/integrations/mocked/src/modules/carts/carts.mapper.ts-304-316 (1)
304-316:⚠️ Potential issue | 🟡 MinorMetadata update on a potentially removed item.
When
data.quantity <= 0, the item is spliced from the array (Line 306), but the code continues to Line 314 where it may updateitem.metadataon the now-detached reference. This is harmless (the detached object is garbage-collected) but misleading. Add anelseorreturnafter the splice branch.Proposed fix
if (data.quantity !== undefined) { if (data.quantity <= 0) { cart.items.data.splice(itemIndex, 1); cart.items.total = cart.items.data.length; } else { item.quantity = data.quantity; item.subtotal = { value: item.price.value * data.quantity, currency: cart.currency }; item.total = { value: item.price.value * data.quantity, currency: cart.currency }; + if (data.metadata !== undefined) { + item.metadata = data.metadata; + } } + } else if (data.metadata !== undefined) { + item.metadata = data.metadata; } - if (data.metadata !== undefined) { - item.metadata = data.metadata; - }packages/integrations/medusajs/src/modules/checkout/checkout.mapper.ts-95-99 (1)
95-99:⚠️ Potential issue | 🟡 MinorUnsafe currency cast — unsupported currency codes pass through silently.
Line 99 casts the raw
currencyCodestring directly toModels.Price.Currencywithout validating it against the supported values (USD, EUR, GBP, PLN). If Medusa returns a valid ISO 4217 currency code that isn't supported (e.g., "JPY", "CHF"), it will silently produce invalid data.Add validation before the cast, either using a supported currencies array or a switch statement:
Proposed fix
const currencyCode = calculatedPrice?.currency_code?.toUpperCase(); if (!currencyCode) { throw new BadRequestException(`Shipping option ${option.id} has no currency information`); } + const supportedCurrencies = ['USD', 'EUR', 'GBP', 'PLN']; + if (!supportedCurrencies.includes(currencyCode)) { + throw new BadRequestException(`Shipping option ${option.id} has unsupported currency: ${currencyCode}`); + } const currency = currencyCode as Models.Price.Currency;Note: This pattern exists in other mappers as well (products, carts, orders).
apps/docs/docs/integrations/commerce/medusa-js/overview.md-58-99 (1)
58-99:⚠️ Potential issue | 🟡 MinorMissing config example for
payments.ts.The modules table (line 147) lists Payments as a supported module, but Step 1 does not include a corresponding
packages/configs/integrations/src/models/payments.tsconfig example. For completeness, consider adding it alongside the carts, checkout, and customers examples — users following this guide would otherwise miss configuring payments.apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-orders.md-134-134 (1)
134-134:⚠️ Potential issue | 🟡 MinorAmbiguous type for
itemsfield.The type column for
itemsshowsdata, totalwhich is unclear for a reader. Other paginated types in this PR's docs use thePagination.Paginated<T>pattern (e.g.,Pagination.Paginated<Order>). Consider using a consistent type reference here.Suggested fix
-| items | data, total | Line items | +| items | Paginated\<OrderItem\> | Paginated line items |packages/integrations/medusajs/src/modules/customers/customers.mapper.ts-14-24 (1)
14-24:⚠️ Potential issue | 🟡 Minor
address_2mapped tostreetNumbermay lose the apartment data.Medusa's
address_2typically represents apartment/suite information, yet here it's mapped tostreetNumber. The framework model has bothstreetNumberandapartmentfields (seepackages/framework/src/utils/models/address.ts), butapartmentis never populated in the inbound mapping. In the reverse mapping (mapAddressToMedusa, Line 49),streetNumber || apartmentmeans theapartmentvalue is dropped ifstreetNumberis present.Consider splitting the mapping or documenting the intentional data loss:
Possible fix for inbound mapping
- streetNumber: medusaAddress.address_2, + apartment: medusaAddress.address_2,packages/integrations/medusajs/src/modules/carts/carts.mapper.ts-131-151 (1)
131-151:⚠️ Potential issue | 🟡 MinorPromotions are mapped with hardcoded
value: 0andtype: 'PERCENTAGE'.Every promotion will report zero value and "PERCENTAGE" type regardless of the actual discount. Consumers relying on
Promotion.valueto display savings will see nothing. If Medusa v2's cart response doesn't expose this data, a comment clarifying that limitation would help. Alternatively, consider computing the value fromcart.discount_totalif only one promotion is applied.packages/integrations/medusajs/src/modules/carts/carts.mapper.ts-89-104 (1)
89-104:⚠️ Potential issue | 🟡 Minor
address_2is mapped to bothstreetNumberandapartment, losing the distinction.Lines 98–99 both read from
address.address_2. Medusa'saddress_1is the primary street line andaddress_2is a secondary line (suite/apartment). Usingaddress_2forstreetNumberseems unintended — it should likely remain empty or be parsed fromaddress_1.🐛 Proposed fix
streetName: address.address_1 ?? '', - streetNumber: address.address_2 ?? '', - apartment: address.address_2 ?? '', + streetNumber: '', + apartment: address.address_2 ?? '',packages/integrations/medusajs/src/modules/carts/carts.service.ts-382-384 (1)
382-384:⚠️ Potential issue | 🟡 MinorRedundant ternary — both branches are identical.
Both branches of the conditional call
this.medusaJsService.getStoreApiHeaders(authorization)with the same argument, making the ternary a no-op. This same pattern is repeated on Lines 478-480.Proposed fix
- const headers = authorization - ? this.medusaJsService.getStoreApiHeaders(authorization) - : this.medusaJsService.getStoreApiHeaders(authorization); + const headers = this.medusaJsService.getStoreApiHeaders(authorization);Apply the same fix at Lines 478-480.
packages/integrations/mocked/src/modules/carts/carts.service.ts-170-184 (1)
170-184:⚠️ Potential issue | 🟡 MinorWrong exception type:
NotFoundExceptionused for a validation error.Lines 171 and 183 throw
NotFoundException("Currency is required when creating a new cart"). A missing required field is a client input error, which should beBadRequestException(HTTP 400), notNotFoundException(HTTP 404).Proposed fix
if (!data.currency) { - throw new NotFoundException('Currency is required when creating a new cart'); + throw new BadRequestException('Currency is required when creating a new cart'); }Apply at both Line 171 and Line 183.
🧹 Nitpick comments (52)
packages/integrations/mocked/src/modules/resources/resources.mapper.ts (2)
397-406: Inconsistent customer ID formats across switch cases.
'cus_01KH3J08TY40PYGVEG3A04CP8R'follows Medusa.js ID conventions while'cust-002'retains the old format. If the intent is to align mocked data with Medusa.js, consider updating the second case as well for consistency.
556-562: Typo:getCastomerId→getCustomerId.Pre-existing, but since this PR touches the call sites, it's a good time to fix it.
Proposed fix
-const getCastomerId = (authorization: string): string | null => { +const getCustomerId = (authorization: string): string | null => {And update references at lines 393 and 472 accordingly.
packages/integrations/medusajs/src/modules/utils/handle-http-error.ts (1)
11-22: Mixed error-propagation styles: synchronousthrowvs.return throwError(…).Lines 15–21 use synchronous
throw, while lines 12–14 and 22 return RxJSthrowErrorobservables. Callers must guard against both a synchronous exception and an observable error from the same function, which is fragile. Consider unifying onreturn throwError(() => …)throughout for consistency.♻️ Suggested unification
export const handleHttpError = (error: any) => { if (error instanceof HttpException) { return throwError(() => error); } if (error.status === 404) { - throw new NotFoundException(`Not found`); + return throwError(() => new NotFoundException('Not found')); } else if (error.status === 403) { - throw new ForbiddenException('Forbidden'); + return throwError(() => new ForbiddenException('Forbidden')); } else if (error.status === 401) { - throw new UnauthorizedException('Unauthorized'); + return throwError(() => new UnauthorizedException('Unauthorized')); } return throwError(() => new InternalServerErrorException(error.message)); };Note: this would require updating the existing tests for 404/403/401 to use
firstValueFrom+rejects.toThrowinstead of synchronousexpect().toThrow.packages/integrations/medusajs/src/modules/products/products.service.spec.ts (2)
156-168: Good edge-case coverage for missing-variant scenarios.These two new tests cover important defensive paths — empty variants and non-matching variant IDs — ensuring the service surfaces clear, actionable error messages.
One minor style note: the inline
as import('@o2s/framework/modules').Products.Request.GetProductParamstype assertions (Lines 165, 178) could be replaced by a top-level import for readability:import type { Products } from '@o2s/framework/modules';Then use
as Products.Request.GetProductParamsinline. This is purely a readability preference.Also applies to: 170-181
55-56: Consider removing the unusedmockHttpClienttype from the SDK-centric tests, or at least documenting why it's still needed.
mockHttpClientis still declared and passed to the constructor but is only exercised bygetRelatedProductList. This is fine since the service constructor requires it, but adding a brief comment (e.g.,// still needed for getRelatedProductList which uses admin API) would help future readers understand the split between SDK and HTTP paths.packages/integrations/medusajs/src/modules/products/products.service.ts (1)
33-35: Tight coupling between field strings and mapper expectations.These field strings are critical — if they drift from what the mapper expects (e.g.,
calculated_price,categories,tags,images,metadata), the mapper will silently produce incomplete data (prices default to 0, missing specs, etc.). Consider adding a brief inline comment mapping each field group to the mapper function that consumes it, or co-locating these constants with the mapper.packages/integrations/medusajs/src/modules/products/products.mapper.ts (3)
49-75: Prefer??over||for numeric fallback.Line 68:
amount: matchingPrice.amount || 0— using||means a legitimate price of0is treated the same asundefined. While0 || 0still evaluates to0, this is coincidental;??conveys the correct intent (fallback only onnull/undefined) and is consistent with the!= nullcheck on Line 55.Proposed fix
- amount: matchingPrice.amount || 0, + amount: matchingPrice.amount ?? 0,
80-113: Missing type validation on parsedkeySpecsitems.After
JSON.parse(Line 95) or the array branch (Line 100), the individual items inkeySpecsare not validated. If the parsed JSON contains objects wherevalueoriconare non-strings (e.g., numbers, nested objects), they'll be passed through to the model undetected.Given this is metadata from an external system, a lightweight runtime check would improve robustness:
Proposed defensive filter
return keySpecs.map((spec) => ({ - value: spec.value, - icon: spec.icon, + value: typeof spec.value === 'string' ? spec.value : String(spec.value ?? ''), + icon: typeof spec.icon === 'string' ? spec.icon : undefined, }));
117-136: Type-safety ofkeyofon union types.
variant[field as keyof AnyProductVariant]on Line 123 works at runtime butkeyof (A | B)yields only the intersection of keys. If anyVARIANT_SPEC_FIELDSentry isn't a shared key betweenStoreProductVariantandAdminProductVariant, TypeScript won't catch it — theascast silences the compiler. This is acceptable here since these are standard Medusa variant fields, but worth a note if the spec fields list grows.packages/integrations/medusajs/src/modules/payments/payments.service.spec.ts (1)
76-83: Minor: duplicate invocations to assert message and type separately.Each
expectcall invokesservice.getProviders(...)again. You could call once, capture the error, and assert both the type and message on the same caught error — reducing redundant execution.packages/integrations/medusajs/src/modules/customers/customers.service.spec.ts (1)
71-81: Minor: same duplicate invocation pattern as in payments spec.Same as noted in the payments spec — the two
expectcalls each invokeservice.getAddresses(undefined)separately. Consider capturing the thrown error once and asserting both the type and message.packages/framework/src/modules/payments/payments.model.ts (1)
16-25: Consider documentingclientSecretsensitivity and serialization expectations.
clientSecret(line 22) is a sensitive field. While Stripe's client secret is designed to be shared with the frontend for payment confirmation, ensure that response serialization doesn't inadvertently expose secrets from other providers where the semantics may differ. A brief doc comment noting the intended audience (client-side only) would help future maintainers.packages/integrations/medusajs/src/modules/customers/customers.service.ts (1)
35-58: Auth checks throw synchronously outside the Observable pipeline.The
throw new UnauthorizedException(...)at lines 37 and 42 executes synchronously before any Observable is returned. While this works with NestJS (the framework catches the thrown exception), it's inconsistent with the RxJS error-handling pattern used in the rest of the method (catchError,throwError). This pattern is repeated in every method.This is fine functionally — just noting it's a stylistic inconsistency. The synchronous throw is arguably preferable here as a "fail fast" guard.
packages/integrations/mocked/src/modules/checkout/checkout.mapper.ts (1)
183-183: Type castas Orders.Model.ShippingMethod[]may hide missing fields.The inline shipping option objects define
id,name,description,total, andsubtotal, butOrders.Model.ShippingMethodmay require additional fields. Theascast suppresses type checking here. Consider either extending the mock objects to match the full type or usingPartial<>/ a dedicated mock factory.packages/framework/src/modules/carts/carts.service.ts (1)
5-6: Unusual variadic constructorprotected constructor(..._services: unknown[]).The variadic
unknown[]constructor discards all arguments. If the intent is to allow subclasses to pass dependencies, a parameterlessprotected constructor()would be clearer — NestJS DI injects into the subclass constructor, not the super call.packages/integrations/medusajs/src/modules/checkout/checkout.mapper.ts (1)
84-86: Unused_defaultCurrencyparameter inmapShippingOption.The
_defaultCurrencyparameter is accepted but never used — the function throws when currency data is missing instead of falling back to the default. Either remove the parameter or use it as a fallback whencurrency_codeis absent.packages/integrations/mocked/src/modules/carts/carts.mapper.ts (2)
7-17: Unsafe type assertions inmapPaymentMethodFromMetadata.Multiple
ascasts without validation (Lines 8, 12-15). Ifmetadata.paymentMethodhas an unexpected shape, this will silently produce a malformedPaymentMethod. Since this is mocked code, impact is low, but consider adding basic guards.
249-254: Silent error swallowing when product is not found.The
catchblock silently returnsundefinedwithout logging. This makes debugging difficult when items fail to add. Consider logging the error.Proposed fix
try { product = mapProduct(data.productId); - } catch { + } catch (error) { + console.warn(`Product not found: ${data.productId}`, error); return undefined; // Product not found }packages/framework/src/modules/checkout/checkout.service.ts (1)
6-6: Replace barrel import with direct imports to avoid circular reference.Line 6:
import * as Checkout from './'creates a circular dependency—the barrel re-exports this very file. While TypeScript handles this at compile time, it's fragile and unconventional. Import directly from the specific modules instead:import * as Request from './checkout.request'; import * as Model from './checkout.model';Then update references from
Checkout.Request.*toRequest.*andCheckout.Model.*toModel.*throughout the class.packages/framework/src/modules/checkout/checkout.request.ts (1)
1-67: Add validation decorators to request DTOs.These DTOs lack
class-validatordecorators, and no globalValidationPipeis configured inmain.tsorapp.module.ts. Without request-level validation, invalid shapes and types will pass through to the service layer unchecked. Either add@IsString(),@IsOptional(),@IsNotEmpty()decorators to enforce request schema, or configure a globalValidationPipein the NestJS bootstrap.packages/configs/integrations/src/models/payments.ts (1)
1-9: Non-null assertion onConfig.payments!relies on mocked integration always defining this field.Since
Configis typed asPartial<ApiConfig['integrations']>, this will throw at runtime if the mocked integration ever removes or fails to define thepaymentsentry. This appears to be a pre-existing pattern across other config modules, so it's consistent — but worth noting the implicit contract.apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-checkout.md (1)
149-180: Mermaid class diagram classes are empty — consider adding key fields.The class bodies are all empty, which reduces the diagram's usefulness as a quick reference. Adding at least the primary fields (e.g.,
cart: Cart,totals: objectonCheckoutSummary) would make the diagram self-documenting alongside the type tables below.apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-carts.md (1)
230-250: Clarify theitemsfield type.Line 236 lists the type of
itemsasdata, total, which reads more like a shape description than a type. Consider using something like{ data: CartItem[], total: number }or a named type alias to be consistent with how other fields are documented.packages/integrations/medusajs/src/modules/checkout/checkout.mapper.spec.ts (1)
72-77: Consider combining the twoexpectcalls per error test to avoid invoking the mapper twice.Each error test calls the mapper function twice — once to check the exception type, once to check the message. You could use a single assertion with
toThrowErroror wrap in a try/catch, though this is a minor style point.♻️ Example consolidation
- expect(() => mapCheckoutSummary(cart)).toThrow(BadRequestException); - expect(() => mapCheckoutSummary(cart)).toThrow('Shipping address is required'); + expect(() => mapCheckoutSummary(cart)).toThrow( + expect.objectContaining({ + message: expect.stringContaining('Shipping address is required'), + }), + );This pattern checks both the exception type (via
objectContaining) and message in one invocation. Apply similarly to the other error tests.apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-customers.md (2)
101-110: Mermaid class diagram has empty classes, providing minimal value.The diagram declares
CustomerAddressandAddresswith no fields. Consider populating them with key fields (e.g.,id,label,isDefault,firstName,city, etc.) to make the diagram self-explanatory without needing to scroll to the types tables below.
65-75: Missing body parameters table forupdateAddress.
createAddress(Line 57) documents its body parameters in a table, butupdateAddressdoes not. Since they likely share a similar shape, consider adding the same detail here for consistency. Similarly,deleteAddressandsetDefaultAddressare missing parameter tables.packages/integrations/medusajs/src/modules/customers/customers.mapper.spec.ts (1)
44-47: Weak date assertions — consider checking exact ISO strings.
toContain('2024')would pass for any string containing "2024". Consider asserting the exact expected value (e.g.,toEqual(new Date('2024-01-01'))or comparing ISO strings) for more deterministic tests.packages/integrations/mocked/src/modules/payments/payments.service.ts (2)
54-68: Redundant null check after successfulfindIndex.When
findIndexreturns a non-negative index (line 54),this.sessions[index]is guaranteed to exist. The check on lines 60–63 is unreachable dead code. The same pattern repeats incancelSession(lines 78–81).♻️ Proposed simplification for updateSession
const index = this.sessions.findIndex((s) => s.id === params.id); if (index === -1) { return throwError(() => new NotFoundException(`Payment session with ID ${params.id} not found`)); } - const existingSession = this.sessions[index]; - if (!existingSession) { - return throwError(() => new NotFoundException(`Payment session with ID ${params.id} not found`)); - } + const existingSession = this.sessions[index]!; const updatedSession = updatePaymentSession(existingSession, data);
22-38: Hardcoded'en'locale when resolving provider increateSession.
getProviderscorrectly usesparams.locale, butcreateSessionon line 26 always passes'en'togetMockProviderById. If a provider only exists in a non-English locale set, the lookup would fail with a falseNotFoundException. For a mock service this is low-risk, but worth noting for consistency.packages/integrations/medusajs/src/modules/checkout/checkout.service.spec.ts (1)
157-174: Consider adding error-path tests forsetShippingMethod.
setAddresseshas tests for cart-not-found and empty-cart, butsetShippingMethodonly tests the happy path. If the service performs similar validation (cart existence, items check) before delegating, those branches are untested here.packages/integrations/medusajs/src/modules/orders/orders.mapper.ts (1)
89-91:streetNumberandapartmentboth map toaddress_2.Both fields are derived from the same source (
address.address_2), which means they'll always hold identical values. If this is intentional (Medusa doesn't distinguish between them), a brief comment would clarify the intent. Otherwise, one of them should use a different source or be left empty.packages/integrations/medusajs/src/modules/customers/customers.mapper.ts (1)
12-12: Add a code comment explaining the name-based label fallback.The
CustomerAddress.labelfield is documented as a semantic label (e.g., "Home", "Work", "Billing"), but Medusa'sStoreCustomerAddresslacks a dedicated label field. Using the person's name as a fallback is pragmatic, but this design decision should be documented in the code comment to clarify why the field's semantic purpose differs from its implementation.packages/integrations/medusajs/src/modules/carts/carts.service.spec.ts (1)
483-502: Redundant double-invocation in "no items" prepareCheckout test.The test calls
firstValueFrom(service.prepareCheckout(...))twice — once to assertBadRequestExceptionand again to assert the message string. Each call re-executes the service method (and the underlying mock). Combine both assertions on a single call:♻️ Proposed fix: single invocation with chained assertion
it('should throw BadRequestException when cart has no items', async () => { mockSdk.store.cart.retrieve.mockResolvedValue({ cart: minimalCart }); - await expect( - firstValueFrom( - service.prepareCheckout( - { cartId: 'cart_1' } as Carts.Request.PrepareCheckoutParams, - 'Bearer token', - ), - ), - ).rejects.toThrow(BadRequestException); - await expect( - firstValueFrom( - service.prepareCheckout( - { cartId: 'cart_1' } as Carts.Request.PrepareCheckoutParams, - 'Bearer token', - ), - ), - ).rejects.toThrow('Cart must have items before preparing checkout'); + await expect( + firstValueFrom( + service.prepareCheckout( + { cartId: 'cart_1' } as Carts.Request.PrepareCheckoutParams, + 'Bearer token', + ), + ), + ).rejects.toThrow( + expect.objectContaining({ + message: 'Cart must have items before preparing checkout', + }), + ); });packages/integrations/medusajs/src/modules/payments/payments.mapper.ts (2)
5-14:requiresRedirectrelies on fragile substring matching against provider IDs.If Medusa is configured with a custom payment provider whose ID doesn't contain
'stripe'or'paypal'but still requires redirect (e.g., an Adyen redirect integration), this will incorrectly returnfalse. Consider extending the check or derivingrequiresRedirectfrommapProviderTypeto keep the logic centralized:♻️ Proposed refactor
export function mapPaymentProvider(medusaProvider: HttpTypes.StorePaymentProvider): Payments.Model.PaymentProvider { + const type = mapProviderType(medusaProvider.id); return { id: medusaProvider.id, name: medusaProvider.id, // Medusa doesn't provide a name, use ID - type: mapProviderType(medusaProvider.id), + type, isEnabled: true, // Assume enabled if returned by API - requiresRedirect: medusaProvider.id.includes('stripe') || medusaProvider.id.includes('paypal'), + requiresRedirect: type === 'STRIPE' || type === 'PAYPAL' || type === 'ADYEN', config: {}, }; }
54-67: Null/undefinedstatuswould cause a runtime crash on.toUpperCase().If Medusa ever returns a payment session with a missing or null
status,status.toUpperCase()will throw aTypeError. A defensive guard would prevent this:🛡️ Proposed fix
function mapPaymentSessionStatus(status: string): Payments.Model.PaymentSessionStatus { - switch (status.toUpperCase()) { + switch ((status ?? '').toUpperCase()) {packages/integrations/mocked/src/modules/payments/mocks/providers.mock.ts (2)
16-58: Consider extracting shared provider definitions to reduce duplication.The
stripeandpaypalentries are identical across all three locales — onlysystem.namediffers. You could define a base array and override the localizedsystementry per locale. That said, this is mock data, so the duplication is tolerable if readability is preferred.
76-83:'BANK_TRANSFER'is listed in the return type but never produced.
providerTypeToPaymentMethodTypedeclares'BANK_TRANSFER'in its return union, but no branch returns it. If it's intentionally reserved for future use, a brief comment would clarify; otherwise, remove it from the type to avoid confusion.packages/integrations/medusajs/src/modules/carts/carts.mapper.ts (2)
15-15:_defaultCurrencyparameter is accepted but unused.The underscore prefix suppresses the linter warning, but this parameter was presumably intended as a fallback when
cart.currency_codeis missing. Consider using it in the fallback path at line 16–18, or removing it from the signature if truly unnecessary.♻️ Proposed fix: use as fallback
-export const mapCart = (cart: HttpTypes.StoreCart, _defaultCurrency: string): Carts.Model.Cart => { +export const mapCart = (cart: HttpTypes.StoreCart, defaultCurrency: string): Carts.Model.Cart => { - if (!cart.currency_code) { - throw new Error(`Cart ${cart.id} has no currency code`); - } - const currency = cart.currency_code as Models.Price.Currency; + const rawCurrency = cart.currency_code || defaultCurrency; + if (!rawCurrency) { + throw new Error(`Cart ${cart.id} has no currency code`); + } + const currency = rawCurrency.toUpperCase() as Models.Price.Currency;
26-27:Date.toString()produces a non-ISO format when the value is aDateobject.If
cart.created_atis aDateinstance (as in the test fixture),.toString()returns a locale-dependent string like"Mon Jan 01 2024 ...", not ISO 8601. The fallback uses.toISOString(), creating an inconsistency. If the Medusa SDK deserializes timestamps asDateobjects rather than strings, downstream consumers expecting ISO strings will be affected.♻️ Proposed fix
- createdAt: cart.created_at?.toString() ?? new Date().toISOString(), - updatedAt: cart.updated_at?.toString() ?? new Date().toISOString(), + createdAt: cart.created_at instanceof Date ? cart.created_at.toISOString() : (cart.created_at ?? new Date().toISOString()), + updatedAt: cart.updated_at instanceof Date ? cart.updated_at.toISOString() : (cart.updated_at ?? new Date().toISOString()),packages/framework/src/modules/customers/customers.controller.ts (1)
9-12: Add auth decorator to controller to align with framework pattern.CustomersController lacks the
@Auth.Decorators.Rolesmetadata decorator that UsersController applies to its endpoints. While the service layer validatesheaders.authorizationand throwsUnauthorizedException, auth concerns should be declared at the controller level for consistency and clarity. Apply@Auth.Decorators.Roles({ roles: [] })to each endpoint to match the established pattern in UsersController.packages/integrations/mocked/src/modules/payments/payments.mapper.ts (1)
28-28: Consider adding randomness to the ID to avoid collisions.
Date.now()alone can produce duplicate IDs under concurrent calls. ThecreateCustomerAddressmapper inpackages/integrations/mocked/src/modules/customers/customers.mapper.tsuses a more robust pattern withMath.random().toString(36)appended.🔧 Proposed fix
- const id = `ps_${Date.now()}`; + const id = `ps_${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;packages/integrations/mocked/src/modules/customers/customers.service.ts (2)
89-98: Redundant null check after a successfulfindIndex.Lines 95-98 check
existingAddressfor truthiness immediately after confirmingindex !== -1. In single-threaded JS,this.addresses[index]is guaranteed to exist iffindIndexjust returned a valid index. The same pattern repeats insetDefaultAddress(lines 148-151).♻️ Simplified version
const index = this.addresses.findIndex((addr) => addr.id === params.id && addr.customerId === customerId); if (index === -1) { return throwError(() => new NotFoundException(`Address with ID ${params.id} not found`)); } - const existingAddress = this.addresses[index]; - if (!existingAddress) { - return throwError(() => new NotFoundException(`Address with ID ${params.id} not found`)); - } + const existingAddress = this.addresses[index]!;
16-16:implementsvsextends— same consistency note as the mockedCheckoutService.The framework's
CustomerServiceis an abstract class. Usingimplementsworks but skips the base constructor. For consistency with the MedusaJS integrations (which useextends), consider switching.packages/integrations/medusajs/src/modules/checkout/checkout.service.ts (2)
209-216: Direct mutation of the mappedorderobject.
order.email = emailmutates the object returned bymapOrder. This works but can be surprising ifmapOrderis ever expected to produce an immutable result. A spread would be safer:♻️ Immutable alternative
- const order = mapOrder(response.order, this.defaultCurrency); - - // Attach email for order confirmation if provided (guest checkout) - if (email) { - order.email = email; - } - - return of(mapPlaceOrderResponse(order)); + const order = mapOrder(response.order, this.defaultCurrency); + const orderWithEmail = email ? { ...order, email } : order; + + return of(mapPlaceOrderResponse(orderWithEmail));
23-35: Remove unusedauthServiceandcustomersServiceinjections.
this.authServiceandthis.customersServiceare never referenced. Also, unlikeOrdersServicewhich throws ifDEFAULT_CURRENCYis empty (lines 43-45 of orders.service.ts),CheckoutServicesilently defaults to an empty string on line 34. Align the validation behavior by either adding a guard or documenting why an empty default is acceptable here.packages/integrations/medusajs/src/modules/payments/payments.service.ts (2)
46-69: Swallowing all errors ingetProvidersmay hide real issues.The
catchErroron Line 63 catches all errors (including 401/403/500) and silently returns an empty provider list. This makes sense for graceful degradation, but authentication or server errors will be invisible to callers. Consider re-throwing non-retriable errors (401, 403) so they propagate correctly.Proposed fix: re-throw auth errors
catchError((error) => { - // If endpoint doesn't exist or fails, return empty list - this.logger.warn('Failed to fetch payment providers from Medusa', error); - return of(mapPaymentProviders([], 10, 0)); + if (error.response?.status === 401 || error.response?.status === 403) { + return handleHttpError(error); + } + this.logger.warn('Failed to fetch payment providers from Medusa', error); + return of(mapPaymentProviders([], 10, 0)); }),
116-130:getSessionalways returnsundefined— consider a clearer contract.The method is fully stubbed and will always return
undefined. The extensive inline comments explain why, which is good. However, if this is a known limitation, consider throwingNotImplementedException(consistent with howgetCartListandgetCurrentCarthandle unsupported operations in the carts service) rather than silently returningundefined, which callers may misinterpret as "session not found."packages/integrations/medusajs/src/modules/carts/carts.service.ts (3)
521-541:resolveBillingAddressis unused dead code.This private method duplicates the billing address resolution logic that is already inlined in
updateCartAddresses(Lines 402-414). It is never called anywhere in the file.Remove the dead method, or refactor `updateCartAddresses` to use it
Option A — Remove it:
- /** - * Resolves the billing address from the request data. - * Returns `null` if no billing address is specified (caller should fall back to shipping address). - */ - private resolveBillingAddress( - data: Carts.Request.UpdateCartAddressesBody, - authorization: string | undefined, - ): Observable<HttpTypes.StoreAddAddress | null> { - if (data.billingAddressId && authorization) { - return this.customersService.getAddress({ id: data.billingAddressId }, authorization).pipe( - map((billingAddress) => { - if (!billingAddress) { - throw new NotFoundException(`Address with ID ${data.billingAddressId} not found`); - } - return mapAddressToMedusa(billingAddress.address); - }), - ); - } - - if (data.billingAddress) { - return of(mapAddressToMedusa(data.billingAddress)); - } - - return of(null); - }Option B — Use it in
updateCartAddressesto reduce inline duplication.
136-140:deleteCartsilently succeeds without performing any operation.This returns
of(undefined)without making any API call to Medusa, which means the cart remains intact on the server. While there's a debug log, callers will believe the deletion succeeded. Consider throwingNotImplementedException(likegetCartListandgetCurrentCart) to make the limitation explicit.Proposed fix
deleteCart(_params: Carts.Request.DeleteCartParams, _authorization?: string): Observable<void> { - this.logger.debug('Delete cart operation - not directly supported in Medusa Store API'); - - return of(undefined as void); + return throwError( + () => + new NotImplementedException( + 'Delete cart is not supported in Medusa.js integration. Medusa Store API does not provide a cart deletion endpoint.', + ), + ); }
30-44: Constructor throws synchronously on missing config, which crashes the DI container.Line 42 throws a plain
ErrorifDEFAULT_CURRENCYis not set. In NestJS, this will crash the entire application during bootstrap rather than providing a clear diagnostic. This is a valid guard but worth noting — aConfigServicevalidation (e.g., via@nestjs/configschema validation) would catch this earlier with a better error message.packages/framework/src/modules/carts/carts.request.ts (1)
50-58:quantitylacks a minimum value constraint.
AddCartItemBody.quantityis typed asnumberwith no minimum constraint. A value of0or negative would be semantically invalid. The Medusa integration'saddCartItempasses it directly to the SDK without validation.packages/integrations/mocked/src/modules/carts/carts.service.ts (1)
30-48: Repeated authorization boilerplate across 10+ methods.Every mutating method repeats the same pattern: fetch cart → check
customerId→ validate authorization → check ownership. Consider extracting a private helper likeassertCartAccess(cartId, authorization)that returns the cart or throws.Example helper
private assertCartAccess( cartId: string, authorization: string | undefined, ): Carts.Model.Cart { const cart = mapCart({ id: cartId }); if (!cart) { throw new NotFoundException('Cart not found'); } if (cart.customerId) { if (!authorization) { throw new UnauthorizedException('Authentication required to access this cart'); } const customerId = this.authService.getCustomerId(authorization); if (cart.customerId !== customerId) { throw new UnauthorizedException('Unauthorized to access this cart'); } } return cart; }Also applies to: 76-104, 106-130, 204-232, 234-261, 263-291, 293-320, 338-365, 457-506
- Replaced hardcoded URLs with dynamic inputs for better flexibility in checkout. - Enhanced promotion handling with direct API invocation for addition and removal. - Refactored `carts.mapper` to standardize currency to uppercase.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Fix all issues with AI agents
In `@packages/framework/src/modules/carts/carts.request.ts`:
- Around line 50-58: The DTO AddCartItemBody currently marks productId as
required and variantId optional, but Medusa's implementation
(CartsService.addCartItem) requires variantId and ignores productId; update
AddCartItemBody to make variantId required and productId optional (or remove
productId requirement) so the DTO matches concrete integrations, and then update
any call sites, tests, and validation that reference AddCartItemBody or expect
productId to be required to use variantId instead; ensure
CartsService.addCartItem and any other integration code still accept the DTO
shape.
In `@packages/integrations/medusajs/src/modules/carts/carts.mapper.ts`:
- Around line 96-99: The mapping currently assigns address.address_2 to both
streetNumber and apartment (see region: address.province, streetName:
address.address_1, streetNumber: address.address_2, apartment:
address.address_2), which duplicates the same value; change the mapper so
address.address_2 is mapped only to apartment (e.g., apartment:
address.address_2 ?? '' ) and set streetNumber to undefined or an empty string
(e.g., streetNumber: undefined or ''), updating the mapping function in
carts.mapper.ts where these symbols are defined.
- Line 39: The Cart mapping currently uses an unsafe cast (mapPrice(... ) as
Models.Price.Price) which can return undefined while Cart.total (and other price
fields mapped similarly) are non-optional; update the mapper to handle missing
prices by either throwing a clear error or supplying a safe fallback (e.g., {
value: 0, currency }) before assigning to Cart.total and the other affected
fields (the occurrences calling mapPrice for total and the other price fields).
Locate the calls to mapPrice in the carts mapper (references to mapPrice and
Cart.total / Models.Price.Price) and replace the direct cast with a guard that
checks the result and either throws a descriptive error or returns/uses the
fallback price object so the Cart model always receives a valid
Models.Price.Price.
In `@packages/integrations/medusajs/src/modules/carts/carts.service.spec.ts`:
- Around line 441-462: The removePromotion flow is using method: 'POST' but
Medusa v2 requires DELETE with a body containing promo_codes; update the service
method removePromotion (in CartsService / carts.service.ts) to call
mockSdk.client.fetch (or client.fetch) with method: 'DELETE' and body: {
promo_codes: [code] } for the '/store/carts/{cartId}/promotions' endpoint, and
update the unit test removePromotion in carts.service.spec.ts to expect method:
'DELETE' and promo_codes array in the body; keep the endpoint and response
assertions the same.
In `@packages/integrations/medusajs/src/modules/carts/carts.service.ts`:
- Around line 142-145: The addCartItem method currently throws
BadRequestException synchronously (in addCartItem) which breaks the Observable
contract; change both synchronous throws (the initial variantId check in
addCartItem and the similar check later around the 183–185 area) to return
throwError(() => new BadRequestException('...')) so errors are emitted through
the Observable stream instead of thrown; update callers/tests that expect a sync
throw to instead await the Observable (e.g.,
firstValueFrom(service.addCartItem(...))) and assert rejects.toThrow when
needed.
- Around line 356-358: The ternary assigning headers is dead code because both
branches call the same method; replace the conditional with a single direct call
to this.medusaJsService.getStoreApiHeaders(authorization) when setting headers
(look for the variable headers in carts.service.ts and the getStoreApiHeaders
method), and apply the same simplification to the identical pattern found later
in the file (the repeated ternary at the second occurrence).
- Around line 259-275: The removePromotion function is calling the wrong HTTP
method and re-applies promo codes; change the request in removePromotion to use
method: 'DELETE' (still sending body: { promo_codes: [params.code] } and the
same headers from medusaJsService.getStoreApiHeaders(authorization)) so the
Medusa Store API removes the promotion; keep the response mapping
(mapCart(response.cart, this.defaultCurrency)) and the error handling
(handleHttpError) unchanged.
In `@packages/integrations/mocked/src/modules/carts/carts.mapper.ts`:
- Around line 388-416: In recalculateCartTotals, clamp the computed
discountTotal so it never exceeds the cart subtotal (e.g., discountTotal =
Math.min(discountTotal, subtotal)) and apply rounding after clamping; compute
taxTotal using the non-negative taxable base (Math.max(subtotal - discountTotal,
0)) and ensure the final total is also clamped to non-negative before assigning
to cart.total; update the assignments for cart.discountTotal, cart.taxTotal and
cart.total accordingly in the recalculateCartTotals function to prevent negative
tax or total values.
- Around line 304-316: The metadata update is being applied to a removed item
when data.quantity <= 0; inside the quantity-handling block in carts.mapper.ts
(the if (data.quantity !== undefined) branch that uses
cart.items.data.splice(itemIndex, 1) and sets cart.items.total), prevent further
updates to the detached item by returning early or adding an else guard so the
subsequent metadata assignment (item.metadata = data.metadata) only runs when
the item was not spliced out; ensure cart.items.total remains correct and no
metadata is written to the removed item reference.
In `@packages/integrations/mocked/src/modules/checkout/checkout.service.ts`:
- Around line 30-32: The current cart validation uses cart.items and
cart.items.data.length which can throw if cart.items.data is nullish; update the
checks to safely handle missing data (e.g., use optional chaining like
cart.items?.data?.length) where you currently return throwError(() => new
BadRequestException(...)) so the predicate becomes something like "if
(!cart.items?.data || cart.items.data.length === 0)" (or check
cart.items?.data?.length === 0) and apply the same defensive change at the
second occurrence on line 52 in checkout.service.ts to avoid TypeError.
- Around line 82-91: The mocked setPayment implementation is ignoring
caller-provided returnUrl and cancelUrl by hardcoding example URLs when calling
client.createSession; update the call in setPayment to pass data.returnUrl and
data.cancelUrl (instead of the literal 'https://example.com/checkout/return' and
cancel) so the method forwards the request body's URLs to createSession (ensure
null/undefined handling if necessary); references: setPayment and the
.createSession(...) invocation.
🧹 Nitpick comments (8)
packages/integrations/medusajs/src/modules/medusajs/medusajs.service.ts (1)
49-78: Consider validating config eagerly in the constructor or at module bootstrap.The lazy initialization defers config validation to first use, meaning a misconfigured environment (e.g., missing
MEDUSAJS_BASE_URL) won't surface until the first request hits a dependent service. Since all known consumers (OrdersService,CartsService,ProductsService,CheckoutService) callgetSdk()in their constructors anyway, the lazy pattern doesn't actually defer any cost — it just delays the error surface from app startup to module construction time.If the intent is to allow the module to load without Medusa config (e.g., when Medusa integration is disabled), this is fine as-is. Otherwise, consider adding a NestJS
OnModuleInithook to fail fast at startup:Optional: eager validation via OnModuleInit
+import { OnModuleInit } from '@nestjs/common'; + `@Global`() `@Injectable`() -export class MedusaJsService { +export class MedusaJsService implements OnModuleInit { // ... existing fields and constructor ... + onModuleInit(): void { + this.ensureInitialized(); + } + // ... rest of methods ... }packages/integrations/medusajs/src/modules/payments/payments.service.spec.ts (3)
76-83: Nit: Consolidate the two throw assertions into one block.Both assertions invoke the same function call. You could verify the exception type and message in a single assertion or at least avoid calling the function under test twice.
♻️ Suggested consolidation
- it('should throw BadRequestException when regionId is missing', () => { - expect(() => service.getProviders({} as Payments.Request.GetProvidersParams, 'Bearer token')).toThrow( - BadRequestException, - ); - expect(() => service.getProviders({} as Payments.Request.GetProvidersParams, 'Bearer token')).toThrow( - 'regionId is required', - ); + it('should throw BadRequestException when regionId is missing', () => { + expect(() => service.getProviders({} as Payments.Request.GetProvidersParams, 'Bearer token')).toThrow( + new BadRequestException('regionId is required'), + ); });
194-194: Nit: Unnecessaryas stringcast on a string literal.
'missing'is already of typestring; the cast is redundant.
202-209: Stub implementation acknowledged — consider adding a TODO or tracking issue.
getSessionalways returnsundefined, which suggests it's a placeholder. A comment or tracked issue would help ensure this doesn't get forgotten.packages/integrations/mocked/src/modules/checkout/checkout.service.ts (1)
172-173: Shared mutableMOCKED_ORDERSarray grows unboundedly and leaks state across tests.Each
placeOrdercall appends to a module-level array that is never cleared. In a long-running test suite this causes inter-test coupling and unbounded memory growth. Consider exposing a reset helper or scoping the store per test.packages/integrations/medusajs/src/modules/carts/carts.service.spec.ts (1)
1-6: Minor: consolidate the two@nestjs/commonimport lines.Lines 2 and 3 import from the same module. Merge them into a single import statement.
♻️ Suggested fix
-import { BadRequestException } from '@nestjs/common'; -import { NotFoundException, NotImplementedException, UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, NotFoundException, NotImplementedException, UnauthorizedException } from '@nestjs/common';packages/integrations/medusajs/src/modules/carts/carts.service.ts (1)
495-515: Remove the deadresolveBillingAddressmethod.This private method (lines 495–515) is never called anywhere in the codebase, and its logic is already duplicated inline in
updateCartAddresses(lines 376–388). Both implementations follow the same pattern: resolve a billing address by ID or use the provided address, with no effective difference.Removal diff
- /** - * Resolves the billing address from the request data. - * Returns `null` if no billing address is specified (caller should fall back to shipping address). - */ - private resolveBillingAddress( - data: Carts.Request.UpdateCartAddressesBody, - authorization?: string, - ): Observable<HttpTypes.StoreAddAddress | null> { - if (data.billingAddressId && authorization) { - return this.customersService.getAddress({ id: data.billingAddressId }, authorization).pipe( - map((billingAddress) => { - if (!billingAddress) { - throw new NotFoundException(`Address with ID ${data.billingAddressId} not found`); - } - return mapAddressToMedusa(billingAddress.address); - }), - ); - } - - if (data.billingAddress) { - return of(mapAddressToMedusa(data.billingAddress)); - } - - return of(null); - }packages/framework/src/modules/carts/carts.request.ts (1)
1-115: Noclass-validatordecorators on DTO classes — runtime validation is not enforced by NestJS.All DTO classes in the framework (carts, customers, articles, products, etc.) rely on TypeScript's
!non-null assertions for required fields, which are compile-time only. Without decorators like@IsString(),@IsNotEmpty(), etc., NestJS'sValidationPipewon't validate or reject malformed requests. Additionally,class-validatoris not installed as a dependency—adding decorators to this file alone would require also installingclass-validatorandclass-transformerpackages. This approach is consistent across all request DTOs in the framework; if validation enforcement is desired, consider installing the necessary packages and adding decorators systematically, or clarify that validation is intentionally handled elsewhere.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@packages/integrations/mocked/src/modules/orders/orders.mapper.ts`:
- Around line 507-558: In mapOrderFromCart, the guest email on the Cart
(cart.email) is being ignored in favor of the optional email parameter; update
the function to set the returned order's email to the provided email if present,
otherwise fall back to cart.email (e.g., email ?? cart.email) so guest checkout
emails from cart are preserved; locate the email assignment in the returned
object (currently "email,") and replace it with the fallback expression,
ensuring typing/nullability is respected.
🧹 Nitpick comments (2)
.changeset/silent-bees-play.md (1)
5-5: Consider adding more detail to the changeset description.The current description is functional but could be enhanced to help consumers better understand the scope. Consider mentioning that this introduces new integration modules with services, controllers, and data models.
Example:
-Mock integrations updated with carts/customers/payments/checkout mocks and enhanced product/order handling. +Mock integrations updated with new modules for carts, customers, payments, and checkout, including mock services, controllers, and data models. Enhanced product and order handling..changeset/lucky-sides-battle.md (1)
5-9: Consider enriching the changeset description with implementation details.The changeset provides a good high-level summary but could be more informative for users reviewing the changelog. Consider adding:
- Integration support: Mention that this includes Medusa.js integration (per PR title) and mocked implementations
- Module structure: Note that this adds four new integration modules (Carts, Checkout, Customers, Payments)
- Configuration: Indicate if any new configuration is required in
AppConfig- Framework vs Integration: Clarify that this includes both framework abstractions and concrete integrations
This is especially important since the PR description is currently a placeholder.
📝 Example enhanced description
Added the normalized data model for a cart/checkout system with full CRUD operations for items and promotions: +- New integration modules: Carts, Checkout, Customers, and Payments +- Medusa.js integration and mocked implementation - Checkout flow supporting address, shipping, and payment setup - Customer address management for authenticated users - Payment provider integration and session handling +- Framework abstractions with REST controllers and services
…module - Updated models, mappers, and service logic to use `sku` for identifying product variants. - Adjusted tests and mocked data mappings to align with the new `sku` field. - Revised documentation to reflect the parameter changes and improve consistency.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/integrations/mocked/src/modules/orders/orders.mapper.ts (1)
6-106:⚠️ Potential issue | 🟡 MinorAll mock products share the identical SKU
'ABC-12345-S-BL'.Every entry in
PRODUCT_DATAuses the sameskuvalue, which undermines any testing that relies on distinguishing products by SKU. If the intent of the refactor was to useskuas the primary identifier (per the commit message), the mock data should have unique SKUs.
🤖 Fix all issues with AI agents
In `@packages/integrations/mocked/src/modules/carts/carts.mapper.ts`:
- Around line 243-248: The call to mapProductBySku in addCartItem is missing a
locale, causing items to always use MOCK_PRODUCTS_EN; update addCartItem to
derive the correct locale (either from the incoming AddCartItemBody if present
or from the cart's regionId) and pass it into mapProductBySku (e.g.,
mapProductBySku(data.sku, locale)); ensure you adjust any function signature or
overloads if needed and update all call sites so product
names/descriptions/prices come from the correct locale-aware mock data instead
of always defaulting to EN.
- Around line 56-58: buildCartItemFromProduct currently creates item IDs using
the cart.items.data.length-derived itemIndex (e.g.,
`ITEM-${itemIndex.toString().padStart(3,'0')}`), which can collide after
removals; change the ID generation in buildCartItemFromProduct (and the similar
occurrence around lines referenced 264-267) to use a monotonic counter or
crypto.randomUUID() (matching how cart IDs are created) instead of the cart
length so newly added items cannot reuse existing IDs; update any tests or mocks
that assert the old format if necessary.
🧹 Nitpick comments (5)
packages/integrations/mocked/src/modules/products/products.mapper.ts (1)
22-35: Consider extracting the repeated locale-source resolution into a shared helper.The locale → source selection block is now duplicated four times in this file (
mapProduct,mapProductBySku,mapProducts,mapRelatedProducts). A small helper likegetProductsSource(locale?: string)would eliminate the repetition.♻️ Suggested helper
+const getProductsSource = (locale?: string): Products.Model.Product[] => { + if (locale === 'pl') return MOCK_PRODUCTS_PL; + if (locale === 'de') return MOCK_PRODUCTS_DE; + return MOCK_PRODUCTS_EN; +}; + export const mapProductBySku = (sku: string, locale?: string): Products.Model.Product => { - let productsSource = MOCK_PRODUCTS_EN; - if (locale === 'pl') { - productsSource = MOCK_PRODUCTS_PL; - } else if (locale === 'de') { - productsSource = MOCK_PRODUCTS_DE; - } - + const productsSource = getProductsSource(locale); const product = productsSource.find((product) => product.sku === sku);packages/integrations/mocked/src/modules/carts/carts.mapper.ts (3)
7-17: No runtime validation onstoredshape — silent type coercion.
mapPaymentMethodFromMetadatacastsstored.id,stored.name, etc. asstringwithout checking they exist or are actually strings. If metadata has a malformedpaymentMethodobject (e.g., missingid), this will silently produce aPaymentMethodwithundefinedfields cast asstring. Acceptable for a mock, but a guard on required fields (id,name) would make debugging easier.
169-219:updateCartsilently drops an invalidpaymentMethodIdwith no feedback.On line 197-198, if
data.paymentMethodIdis provided butgetPaymentMethodDisplayreturnsundefined(unknown provider), the result falls through tomapPaymentMethodFromMetadata(mergedMetadata) ?? cart.paymentMethod, silently keeping the old payment method. This could confuse callers who expect the update to either succeed or error. Consider returningundefinedor throwing to signal the invalid ID — though for a mock this may be acceptable.
332-357:applyPromotionreturnsundefinedfor an unknown code — same as "cart not found".Both "cart not found" (line 337) and "unknown promo code" (line 341) return
undefined, making it impossible for the caller to distinguish the two failure modes. Returning a distinct error or the cart unchanged (with an error flag) for an invalid code would improve debuggability.packages/integrations/medusajs/src/modules/carts/carts.service.ts (1)
492-512: Remove unusedresolveBillingAddressmethodThe
resolveBillingAddressprivate method (lines 492–512) is never called. The billing address resolution logic is fully duplicated inline inupdateCartAddresses(lines 373–385). Delete this unused method.
- Updated request parameters across cart-related models to include `locale` field. - Fixed incorrect HTTP method in promotion removal logic. - Updated checkout service to accept dynamic `returnUrl` and `cancelUrl` values.
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In `@packages/integrations/medusajs/src/modules/carts/carts.service.ts`:
- Around line 155-160: The ownership check inside the switchMap callback
(condition: cart.customerId && authorization && cart.customerId !== customerId)
is throwing a misleading BadRequestException with message 'Variant ID is
required for Medusa carts'; change this to throw a proper authorization error
(use ForbiddenException or UnauthorizedException) and update the message to
indicate the cart belongs to a different customer (e.g., 'Cart does not belong
to the authenticated customer' or similar) in the throwError call so
mapCart/authorization logic reports correct ownership failure.
In `@packages/integrations/medusajs/src/modules/checkout/checkout.mapper.ts`:
- Around line 8-11: The _paymentSession parameter on mapCheckoutSummary is
declared but never used; either implement mapping logic to include payment
session details (e.g., extract payment method id/type from the
Payments.Model.PaymentSession and populate the corresponding field on the
returned Checkout.Model.CheckoutSummary, with null checks) so
mapCheckoutSummary(cart, session) actually uses the session, or explicitly
document the intentional unused parameter by adding a short TODO comment inside
mapCheckoutSummary noting that _paymentSession is reserved for future use;
update the function body accordingly (referencing mapCheckoutSummary and the
_paymentSession parameter).
- Around line 86-101: The function mapShippingOption declares an unused
parameter _defaultCurrency and currently throws BadRequestException when
calculatedPrice?.currency_code is missing; either remove _defaultCurrency (and
update callers) if strict behavior is intended, or implement a fallback by
replacing the currency lookup with something like const currencyCode =
(calculatedPrice?.currency_code ?? _defaultCurrency)?.toUpperCase() and then use
parseCurrency(currencyCode) (and avoid throwing when fallback provides a value);
modify mapShippingOption and any call sites accordingly and ensure
BadRequestException is only thrown if both currency_code and _defaultCurrency
are absent.
In `@packages/integrations/medusajs/src/modules/checkout/checkout.service.ts`:
- Around line 295-347: completeCheckout currently calls setPayment
unconditionally which will break when data.paymentProviderId is missing; add the
same guard pattern used for shippingMethodId so that setPayment is only invoked
when data.paymentProviderId is present (otherwise return of(null) or noop in the
switchMap). Update the pipeline in completeCheckout to check
data.paymentProviderId before calling this.setPayment and ensure the payload
passed to setPayment still uses data.paymentProviderId, returnUrl, cancelUrl,
and metadata when present.
- Around line 161-192: In placeOrder, after the existing
shipping/billing/shippingMethod checks add a pre-check for payment by verifying
the cart has a configured payment method (e.g., cart.paymentMethod or
cart.paymentMethods as appropriate) and return throwError(() => new
BadRequestException('Payment method is required')) if missing; update the
control flow so the early email-update path still calls
completeCartAndCreateOrder(params.cartId, email, authorization) only when the
payment check passes. Ensure you reference placeOrder, cartsService.getCart, and
completeCartAndCreateOrder when locating where to insert the check.
In `@packages/integrations/medusajs/src/modules/resources/resources.mapper.ts`:
- Around line 92-93: The mapAddress function's parameter type is too
narrow—change the signature of mapAddress(address: AddressDTOWithNames) to
accept address: AddressDTOWithNames | null | undefined (or make
AddressDTOWithNames nullable) so it matches actual call sites that pass
asset.address (which can be null); update the function declaration for
mapAddress and any exported types/usages that rely on it so TypeScript no longer
errors while keeping the existing runtime guard if (!address) return undefined.
In `@packages/integrations/medusajs/src/utils/price.ts`:
- Around line 8-10: The current validation in utils/price.ts checks value for
undefined/null and type number but misses NaN and Infinity, so extend the guard
around the variable `value` (the same check that throws `new
Error(\`\${context}: price value is missing or invalid\`)`) to also reject
Number.isNaN(value) and !Number.isFinite(value); update the validation in the
function that uses `value` so it throws the same error for NaN and ±Infinity,
ensuring only finite numeric prices are accepted.
In `@packages/integrations/mocked/src/modules/carts/carts.service.ts`:
- Around line 170-183: Replace the incorrect NotFoundException being thrown when
currency is missing with a BadRequestException to reflect a validation error:
locate the currency-required checks inside carts.service.ts (the branches that
call createCart) and change the throws that read NotFoundException('Currency is
required when creating a new cart') to throw new BadRequestException('Currency
is required when creating a new cart') so the error type matches input
validation failure.
🧹 Nitpick comments (12)
packages/integrations/medusajs/src/modules/carts/carts.service.ts (2)
46-71: Ownership check ingetCartthen re-checked in callers — consider a shared helper.The ownership verification pattern (check
cart.customerId, compare withauthService.getCustomerId) is duplicated ingetCart(lines 54-60),prepareCheckout(lines 291-298), andaddCartItem(lines 158-160). SinceprepareCheckoutalready callsgetCartwhich performs this check, the second check inprepareCheckoutis redundant.
136-140:deleteCartsilently succeeds without actually deleting.The Medusa Store API doesn't support cart deletion, which is understandable — but returning
of(undefined)with only a debug log means callers believe the cart was deleted when it wasn't. Consider returningthrowError(() => new NotImplementedException(...))likegetCartListandgetCurrentCartfor consistency, or at minimum add a warning-level log.packages/integrations/mocked/src/modules/carts/carts.mapper.ts (1)
378-409: Tax and total calculation could yield floating-point drift on large carts.The rounding at line 402 (
Math.round(... * 100) / 100) is applied totaxTotalandtotalbut not tosubtotal. For carts with many items, the accumulated floating-point subtotal may carry precision artifacts. This is minor for a mock but worth noting.packages/integrations/mocked/src/modules/carts/carts.service.ts (1)
30-47: Inconsistent error handling — some methods throw synchronously, others usethrowError().Methods like
getCart(line 39-43) throw synchronously before the Observable is created, whileupdateCartAddresses(line 375-386) uses reactivethrowError(). In NestJS the sync throws work because the framework catches them, but mixing patterns within the same service is confusing and fragile if these methods are ever composed inside other Observables.packages/integrations/medusajs/src/utils/currency.ts (2)
3-3:VALID_CURRENCIESduplicates the frameworkCurrencytype union — consider deriving one from the other.The array
['USD', 'EUR', 'GBP', 'PLN']must be kept in sync withCurrency = 'USD' | 'EUR' | 'GBP' | 'PLN'inpackages/framework/src/utils/models/price.ts. If a currency is added to or removed from the type, this array silently diverges. You could use asatisfiesassertion to get compile-time enforcement:♻️ Suggested improvement
-const VALID_CURRENCIES: Models.Price.Currency[] = ['USD', 'EUR', 'GBP', 'PLN']; +const VALID_CURRENCIES = ['USD', 'EUR', 'GBP', 'PLN'] as const satisfies readonly Models.Price.Currency[];This way, adding a value not in
Currencycauses a compile error — though it won't catch a missing member. If exhaustive coverage matters, a record-based approach would close the gap entirely.
10-10: Consider whether a plainErroris the right exception type here.This throws a generic
Error, whichhandleHttpErrorwill wrap as a 500. If an invalid currency originates from client input, aBadRequestExceptionwould be more appropriate (as done incheckout.mapper.ts). If it's always a server-side data integrity issue, the currentErroris fine. Just flagging the inconsistency across the codebase.packages/integrations/medusajs/src/modules/resources/resources.mapper.ts (2)
80-85: Passing{} as Products.Model.Productcreates a product with no valid fields.When mapping assets within a service instance, the product is an empty object cast to
Products.Model.Product. If any downstream consumer accessesproduct.name,product.price, etc., it will getundefinedat runtime despite the type system saying otherwise. Consider using a dedicated "unknown product" sentinel or making the product field optional on theAssetmodel.
108-126: Silent defaults for unrecognized contract status and payment period — acceptable but worth documenting.
mapContractStatusdefaults to'ACTIVE'andmapPaymentPerioddefaults to'ONE_TIME'for unrecognized input. This is a reasonable approach to avoid runtime errors, but it can mask data quality issues. Alogger.warncall for unrecognized values would make debugging easier in production without breaking the flow.packages/integrations/medusajs/src/modules/products/products.service.ts (1)
81-86: TheArray.isArraycheck on line 84 is unreachable.If
!product?.variants?.lengthon line 81 is falsy (i.e., we pass through), thenproduct.variantsexists and has a truthy.length, which means it's already an array-like with elements. The!Array.isArray(product.variants)check on line 84 can never be true in practice after line 81 passes. Consider removing it to reduce dead code.packages/integrations/medusajs/src/modules/checkout/checkout.service.ts (1)
87-135: Payment method type is hardcoded to'OTHER'.Line 124 always sets
type: 'OTHER'regardless of the actual payment provider. This means the cart's payment method metadata won't reflect the real payment type (e.g., credit card, bank transfer). If downstream code or UI relies on this type for display or logic, it'll always see'OTHER'.packages/integrations/medusajs/src/modules/orders/orders.mapper.ts (2)
114-120:mapPriceparameter type saysnumberbut the function handlesundefined.The runtime check
typeof value === 'undefined'on line 115 is correct defensively, but the function signaturevalue: numberdoesn't reflect this. Call sites may pass potentially-undefined values from Medusa API responses. Aligning the type with actual usage improves type safety:♻️ Proposed fix
-const mapPrice = (value: number, currency: Models.Price.Currency): Models.Price.Price | undefined => { +const mapPrice = (value: number | undefined, currency: Models.Price.Currency): Models.Price.Price | undefined => {
84-99:address_2is mapped to bothstreetNumberandapartment.Lines 93 and 94 both derive from
address.address_2, so they'll always have identical values. This is presumably an approximation since Medusa'sAddressDTOdoesn't distinguish the two. No action required if intentional, but a brief comment would clarify the design decision for future maintainers.
- Added stricter validation for payment and shipping methods in the checkout service. - Refactored currency handling to use default currency when missing. - Standardized import paths for `handleHttpError` utility across modules.
…price mapping for consistent error handling and field validation
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (8)
packages/integrations/medusajs/src/modules/checkout/checkout.service.ts (3)
35-56: Repeated cart-exists-and-has-items validation could be extracted.
setAddresses(lines 41–49) andsetShippingMethod(lines 64–74) share identical validation logic: fetch cart → check existence → check items. A small private helper (e.g.,validateCartHasItems(cartId, authorization)) would reduce duplication.Also applies to: 58-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/integrations/medusajs/src/modules/checkout/checkout.service.ts` around lines 35 - 56, Extract the duplicated "fetch cart → check existence → check items" logic into a private helper, e.g., validateCartHasItems(cartId: string, authorization?: string): Observable<Carts.Model.Cart>, that calls this.cartsService.getCart({ id: cartId }, authorization) and emits the cart or throws NotFoundException/BadRequestException when missing or empty; then replace the duplicated blocks in setAddresses and setShippingMethod to call validateCartHasItems(params.cartId, authorization).pipe(switchMap(cart => /* existing delegate call */)) so both methods reuse the helper and keep existing error handling via catchError(handleHttpError).
219-226: Direct mutation of the freshly mappedorderobject.Line 223 mutates
order.emailaftermapOrderreturns. While safe today since the object isn't reused, this pattern can cause subtle bugs if the flow changes later. Consider passing- const order = mapOrder(response.order, this.defaultCurrency); - - // Attach email for order confirmation if provided (guest checkout) - if (email) { - order.email = email; - } - - return of(mapPlaceOrderResponse(order)); + const order = mapOrder(response.order, this.defaultCurrency); + return of(mapPlaceOrderResponse(email ? { ...order, email } : order));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/integrations/medusajs/src/modules/checkout/checkout.service.ts` around lines 219 - 226, Avoid mutating the mapped order by not assigning to order.email; instead create an order object that includes the email or pass email into the response mapper. Update the code around mapOrder and mapPlaceOrderResponse so you either call mapPlaceOrderResponse({...order, email}) or enhance mapPlaceOrderResponse/order mapper to accept an optional email parameter and incorporate it there, ensuring mapOrder remains pure.
87-135: Inconsistent error handling —setPaymentlackscatchError(handleHttpError).
setAddresses(line 54) andsetShippingMethod(line 83) both wrap their pipelines withcatchError((error) => handleHttpError(error)), butsetPayment,getCheckoutSummary,placeOrder, andcompleteCheckoutdo not. IfhandleHttpErrornormalizes error shapes for the controller layer, its absence here means raw/unexpected errors could leak through.Either apply
handleHttpErrorconsistently across all public methods, or remove it from the two that have it if the controller already handles normalization.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/integrations/medusajs/src/modules/checkout/checkout.service.ts` around lines 87 - 135, The pipeline in setPayment is missing consistent error normalization: add a catchError that invokes handleHttpError (same as used in setAddresses and setShippingMethod) to the observable chain returned by setPayment so any thrown errors are transformed before leaving the service; similarly ensure getCheckoutSummary, placeOrder, and completeCheckout also append catchError(handleHttpError) (or catchError((err) => handleHttpError(err))) to their returned observable chains so all public methods use the same error handling; reference the methods setPayment, setAddresses, setShippingMethod, getCheckoutSummary, placeOrder, completeCheckout and the handleHttpError helper when making the changes.packages/integrations/medusajs/src/modules/resources/resources.mapper.ts (2)
98-106: Same silent-default pattern for payment period — same suggestion applies.Consider adding a warning log here too for consistency. The
replace(/-/g, '_')normalization for hyphenated Medusa values is a nice touch.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/integrations/medusajs/src/modules/resources/resources.mapper.ts` around lines 98 - 106, The mapPaymentPeriod function silently falls back to 'ONE_TIME' when given an unknown paymentType; update mapPaymentPeriod to log a warning when normalization yields a non‑valid period (using the module's logger or an injected logger), include the original paymentType and the normalized value in the message for debugging, and keep using VALID_PAYMENT_PERIODS for the membership check and the existing normalization logic (retain replace(/-/g,'_')) so behavior is unchanged except the added warning before returning 'ONE_TIME'.
88-96: Silent default to'ACTIVE'for unrecognized contract statuses may mask data issues.When an unexpected status value arrives from Medusa, silently falling back to
'ACTIVE'could hide integration bugs or upstream data problems. Consider adding aconsole.warn(or injected logger) when the fallback is triggered so issues are visible in operational logs.💡 Proposed enhancement
function mapContractStatus(status: string | undefined): Resources.Model.ContractStatus { const normalized = (status ?? '').toUpperCase(); if (VALID_CONTRACT_STATUSES.includes(normalized as Resources.Model.ContractStatus)) { return normalized as Resources.Model.ContractStatus; } + // Log to aid debugging unexpected values from Medusa + console.warn(`Unknown contract status "${status}", defaulting to ACTIVE`); return 'ACTIVE'; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/integrations/medusajs/src/modules/resources/resources.mapper.ts` around lines 88 - 96, mapContractStatus currently silently returns the default 'ACTIVE' for unknown statuses; modify it to log a warning when the fallback is used so unexpected Medusa values are visible. In the mapContractStatus function (and using VALID_CONTRACT_STATUSES), detect when status is not included and call a logger (prefer an injected logger if available, else console.warn) with a clear message that includes the original status and the normalized value, then return 'ACTIVE' as before.packages/integrations/medusajs/src/utils/address.ts (1)
31-32:districtandregionare both mapped from the sameprovincefield.If this is intentional (e.g., the framework consumers use the two interchangeably), a brief inline comment would clarify the design choice. Otherwise, consider mapping only one and leaving the other
undefined.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/integrations/medusajs/src/utils/address.ts` around lines 31 - 32, district and region are both assigned from address.province which is likely a mistake or at least confusing; update the mapping in the function that builds the output address so that district and region use the correct source fields (e.g., use address.district for district and address.region for region) or, if they are intentionally the same, add a short inline comment next to the district and region assignments explaining that province is deliberately used for both and why, otherwise leave the unused property undefined instead of duplicating province.packages/integrations/medusajs/src/modules/orders/orders.mapper.ts (1)
54-54:mapProducttakesunitPriceseparately despite always receivingitemwith the same value.The only call site (line 54) passes
item.unit_priceanditemtogether. Sinceitemis always provided and containsunit_price, the separateunitPriceparameter is redundant. The!itemguard on line 63 is dead code in practice.Consider simplifying the signature to
(item: HttpTypes.StoreOrderLineItem, currency: Models.Price.Currency)and readingitem.unit_priceinternally.Also applies to: 58-63
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/integrations/medusajs/src/modules/orders/orders.mapper.ts` at line 54, The mapProduct function currently accepts unitPrice and item separately although callers (e.g., the call at product: mapProduct(item.unit_price, currency, item)) always pass item.unit_price and item together; change mapProduct to a two-argument signature mapProduct(item: HttpTypes.StoreOrderLineItem, currency: Models.Price.Currency) and read unit_price from item internally, remove the redundant unitPrice parameter and the now-dead !item guard inside mapProduct, and update all call sites (including the product mapping call) to pass (item, currency) only so the function usage and internal checks are consistent.packages/integrations/medusajs/src/modules/carts/carts.service.ts (1)
438-453: DoublegetCartcall aftercart.updateadds latency.
updateCartAddressesfirst callsgetCart(line 398), thencart.update(line 439), thengetCartagain (line 446). The update response already contains the updated cart. Consider mapping the update response directly instead of the extra round trip.♻️ Suggested optimization
return from( this.sdk.store.cart.update( params.cartId, cartUpdate, { fields: this.cartItemsFields }, headers, ), ).pipe( - switchMap(() => this.getCart({ id: params.cartId }, authorization)), - map((updatedCart) => { - if (!updatedCart) { - throw new NotFoundException(`Cart with ID ${params.cartId} not found`); - } - return updatedCart; - }), + map((response: HttpTypes.StoreCartResponse) => mapCart(response.cart, this.defaultCurrency)), );
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/integrations/medusajs/src/modules/checkout/checkout.service.ts`:
- Line 32: The code sets this.defaultCurrency =
this.config.get('DEFAULT_CURRENCY') || '' which can pass an empty string into
functions like mapOrder and mapShippingOptions and then into parseCurrency(''),
producing invalid prices; change the initialization in the constructor (or
service init) to fail fast or provide a sensible fallback: either throw an
explicit error if DEFAULT_CURRENCY is missing (e.g., validate and throw in the
constructor of the checkout service) or set a sane default currency constant
(e.g., 'USD') instead of '' so parseCurrency receives a valid code; update any
callers relying on this.defaultCurrency (mapOrder, mapShippingOptions) to assume
a non-empty currency string.
- Around line 275-278: The catchError currently returns the original option
(option.id, calculatedOption: option) which preserves price_type="calculated"
without an amount; update the handler for fulfillment.calculate failures to
either (A) return null/omit the option so it can be filtered out when building
enrichedOptions, or (B) return a clearly marked failed-calculation object (e.g.,
calculatedOption: { ...option, amount: null, price_type: 'unavailable' or
'unknown', calculation_failed: true }) and ensure the enrichedOptions
construction filters nulls or inspects calculation_failed to prevent showing
$0.00/undefined prices; modify the catchError block and the logic that builds
enrichedOptions to implement one of these two behaviors and log the error with
this.logger.warn as before.
In `@packages/integrations/medusajs/src/utils/address.ts`:
- Around line 34-35: The mapping currently sets both streetNumber and apartment
from address.address_2, producing identical values; update the converter so
apartment is sourced from address.address_2 while streetNumber is not (set to
undefined) or explicitly parsed from address.address_1 if you implement parsing
logic; locate the mapping that sets streetNumber and apartment (reference
symbols: streetNumber, apartment, address.address_2, address.address_1) and
change streetNumber to undefined (or add a parseStreetNumber(address.address_1)
call) and keep apartment = address.address_2.
In `@scripts/generate-postman-collection.mjs`:
- Around line 20-23: The authToken variable in the variable array is set to
'Bearer mock-token', which causes a double "Bearer" prefix because Postman's
bearer auth type auto-prepends "Bearer "; change the authToken variable's value
to just 'mock-token' (i.e., update the entry with key: 'authToken' to value:
'mock-token') so the Authorization header becomes "Bearer mock-token" correctly.
---
Duplicate comments:
In `@packages/integrations/medusajs/src/modules/carts/carts.service.ts`:
- Around line 147-150: In addCartItem (which returns
Observable<Carts.Model.Cart>) replace the synchronous throw with an RxJS error
Observable so the error flows through Observable pipelines; specifically, return
throwError(() => new BadRequestException('SKU is required for Medusa carts'))
(and ensure throwError is imported from 'rxjs') so downstream
.pipe(catchError(...)) can catch it consistently like the guard at line 190.
- Around line 466-468: The ternary assigning headers is dead code because both
branches call the same method; replace the ternary with a single call: remove
the conditional and set const headers =
this.medusaJsService.getStoreApiHeaders(authorization); (referencing the headers
variable and the getStoreApiHeaders method in the carts service).
In `@packages/integrations/medusajs/src/modules/resources/resources.mapper.ts`:
- Line 76: The mapping currently passes serviceInstance?.totals?.currency ??
defaultCurrency into parseCurrency which can still accept empty/invalid strings
and yield invalid values; update the currency mapping in resources.mapper.ts to
validate the raw currency string before calling parseCurrency (check non-empty
and membership in the allowed Currency set/enum), or wrap parseCurrency in a
safe helper (e.g., parseCurrencyOrFallback) that tries parseCurrency in a
try/catch and returns a known-good fallback (or defaultCurrency) and logs a
warning; ensure you reference parseCurrency, defaultCurrency, and the
serviceInstance?.totals?.currency source when implementing the check.
In `@packages/integrations/medusajs/src/utils/price.ts`:
- Around line 23-25: The current validation in
packages/integrations/medusajs/src/utils/price.ts only checks for undefined/null
so NaN and Infinity slip through; update the validation around the variable
`value` (the block that throws BadRequestException(`${context}: price value is
missing or invalid`)) to also require a finite number by using
Number.isFinite(value) (or an equivalent finite-number check) and throw the same
BadRequestException when it fails so NaN/Infinity are rejected.
In `@scripts/generate-postman-collection.mjs`:
- Around line 77-82: The current raw URL construction collapses all consecutive
slashes (`.replace(/\/+/g, '/')`) which breaks protocol `://`; change it to
preserve protocol by either trimming trailing slashes from the base token and
joining with the path (e.g., remove trailing slashes from `{{baseUrl}}` then
`/${urlPath.join('/')}`) or use a regex that ignores the protocol (e.g.,
`.replace(/([^:]\/)\/+/g, '$1')`) when building the `raw` value for the `url`
object (see `url`, `urlPath`, and `filteredQuery`).
---
Nitpick comments:
In `@packages/integrations/medusajs/src/modules/checkout/checkout.service.ts`:
- Around line 35-56: Extract the duplicated "fetch cart → check existence →
check items" logic into a private helper, e.g., validateCartHasItems(cartId:
string, authorization?: string): Observable<Carts.Model.Cart>, that calls
this.cartsService.getCart({ id: cartId }, authorization) and emits the cart or
throws NotFoundException/BadRequestException when missing or empty; then replace
the duplicated blocks in setAddresses and setShippingMethod to call
validateCartHasItems(params.cartId, authorization).pipe(switchMap(cart => /*
existing delegate call */)) so both methods reuse the helper and keep existing
error handling via catchError(handleHttpError).
- Around line 219-226: Avoid mutating the mapped order by not assigning to
order.email; instead create an order object that includes the email or pass
email into the response mapper. Update the code around mapOrder and
mapPlaceOrderResponse so you either call mapPlaceOrderResponse({...order,
email}) or enhance mapPlaceOrderResponse/order mapper to accept an optional
email parameter and incorporate it there, ensuring mapOrder remains pure.
- Around line 87-135: The pipeline in setPayment is missing consistent error
normalization: add a catchError that invokes handleHttpError (same as used in
setAddresses and setShippingMethod) to the observable chain returned by
setPayment so any thrown errors are transformed before leaving the service;
similarly ensure getCheckoutSummary, placeOrder, and completeCheckout also
append catchError(handleHttpError) (or catchError((err) =>
handleHttpError(err))) to their returned observable chains so all public methods
use the same error handling; reference the methods setPayment, setAddresses,
setShippingMethod, getCheckoutSummary, placeOrder, completeCheckout and the
handleHttpError helper when making the changes.
In `@packages/integrations/medusajs/src/modules/orders/orders.mapper.ts`:
- Line 54: The mapProduct function currently accepts unitPrice and item
separately although callers (e.g., the call at product:
mapProduct(item.unit_price, currency, item)) always pass item.unit_price and
item together; change mapProduct to a two-argument signature mapProduct(item:
HttpTypes.StoreOrderLineItem, currency: Models.Price.Currency) and read
unit_price from item internally, remove the redundant unitPrice parameter and
the now-dead !item guard inside mapProduct, and update all call sites (including
the product mapping call) to pass (item, currency) only so the function usage
and internal checks are consistent.
In `@packages/integrations/medusajs/src/modules/resources/resources.mapper.ts`:
- Around line 98-106: The mapPaymentPeriod function silently falls back to
'ONE_TIME' when given an unknown paymentType; update mapPaymentPeriod to log a
warning when normalization yields a non‑valid period (using the module's logger
or an injected logger), include the original paymentType and the normalized
value in the message for debugging, and keep using VALID_PAYMENT_PERIODS for the
membership check and the existing normalization logic (retain replace(/-/g,'_'))
so behavior is unchanged except the added warning before returning 'ONE_TIME'.
- Around line 88-96: mapContractStatus currently silently returns the default
'ACTIVE' for unknown statuses; modify it to log a warning when the fallback is
used so unexpected Medusa values are visible. In the mapContractStatus function
(and using VALID_CONTRACT_STATUSES), detect when status is not included and call
a logger (prefer an injected logger if available, else console.warn) with a
clear message that includes the original status and the normalized value, then
return 'ACTIVE' as before.
In `@packages/integrations/medusajs/src/utils/address.ts`:
- Around line 31-32: district and region are both assigned from address.province
which is likely a mistake or at least confusing; update the mapping in the
function that builds the output address so that district and region use the
correct source fields (e.g., use address.district for district and
address.region for region) or, if they are intentionally the same, add a short
inline comment next to the district and region assignments explaining that
province is deliberately used for both and why, otherwise leave the unused
property undefined instead of duplicating province.
…usajs-integration-for-checkout-process # Conflicts: # packages/integrations/medusajs/src/modules/medusajs/medusajs.service.spec.ts # packages/integrations/medusajs/src/modules/medusajs/medusajs.service.ts # packages/integrations/medusajs/src/modules/products/products.mapper.ts # packages/integrations/medusajs/src/modules/products/products.service.spec.ts # packages/integrations/medusajs/src/modules/products/products.service.ts
…d access to ProductList block
…API product methods
…nd remove unused address fields
- add setup and usage guides - update cart and checkout documentation
| const params: HttpTypes.StoreProductListParams = { | ||
| limit: query.limit, | ||
| offset: query.offset, | ||
| status: ['published'], |
There was a problem hiding this comment.
Have you checked if the Store API return only published products by default?
Summary by CodeRabbit
New Features
Documentation