Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
bebe952
🔧 chore: add build:watch script to react-components
Jun 5, 2026
8d2b546
🔧 chore: add build:watch to core, hooks and root workspace
Jun 5, 2026
62113c3
🔒 fix(security): add overrides for high/critical CVEs on v5.0.0
Jun 8, 2026
21962c9
refactor: extract shared address form logic into reusable hooks
Jun 8, 2026
93eb485
fix: remove inline arrow wrappers in providerValues to prevent infini…
Jun 8, 2026
dd1164e
test: add real rapid-form integration test for BillingAddressForm val…
Jun 8, 2026
71116c3
fix: preserve all address fields when pre-filling from API and on use…
Jun 8, 2026
2978cc7
fix: dispatch input event in setValue and resetField so rapid-form ca…
Jun 8, 2026
a49883d
fix: call setValue when state is selected from AddressStateSelector d…
Jun 8, 2026
29d942a
fix: pre-fill state code when country is set for the first time in Ad…
Jun 8, 2026
a81faea
fix: read text input DOM value as fallback when country first detecte…
Jun 9, 2026
03f9adb
fix: expose address reducer values in BillingAddressFormContext/Shipp…
Jun 9, 2026
bed4be6
fix: resolve DTS build error TS4111 for billing/shipping address redu…
Jun 9, 2026
7d9e860
feat: add errorMode prop to BillingAddressForm and ShippingAddressForm
Jun 9, 2026
0cd9b39
test: add coverage tests for errorMode, AddressStateSelector shipping…
Jun 9, 2026
1b8fa9b
test: achieve 100% branch coverage on AddressStateSelector
Jun 9, 2026
5c0f4c9
fix: prevent infinite re-render loop in Shipments component
Jun 9, 2026
d24a1b8
fix: prevent infinite re-render loop in Shipment component
Jun 9, 2026
25a904d
fix: prevent infinite re-render loop in Shipment autoSelect flow
Jun 9, 2026
26ac678
fix: prevent infinite re-render loop in PaymentGateway
Jun 9, 2026
59f490a
fix: prevent infinite re-render loop in PaymentMethodsContainer
Jun 9, 2026
aad6827
fix: move fetchOrder call from useMemo to useEffect in useOrderState
Jun 9, 2026
3552944
feat: make PaymentMethod standalone, deprecate PaymentMethodsContainer
Jun 9, 2026
f3923c0
test: add PaymentMethodsContainer and PaymentMethod specs
Jun 9, 2026
2432677
test: add 100% coverage tests for PaymentMethod, PaymentMethodsContai…
Jun 9, 2026
b5cb599
chore: remove react-doctor workflow and fix pkg-pr-new build step
Jun 9, 2026
516f567
test: add 100% coverage for PlaceOrder, PrivacyAndTermsCheckbox, useP…
Jun 10, 2026
37a2f8a
fix: PlaceOrderButton stays disabled when errors clear but privacy/te…
Jun 10, 2026
5ee6a5b
fix: PlaceOrderButton stays disabled when no payment selected or erro…
Jun 10, 2026
9b150eb
fix: stabilize setPaymentRef and other callbacks in PaymentMethodsCon…
Jun 10, 2026
39ce2e6
fix: PlaceOrderButton must not enable when no payment method is selected
Jun 10, 2026
438079b
fix: AddressCountrySelector always shows pre-filled country
Jun 10, 2026
313f2fd
fix: revert unnecessary setValue empty call on country selector mount
Jun 10, 2026
8f61803
fix: normalize null value to empty string in BaseSelect
Jun 10, 2026
b333cd0
fix: preserve user selection in BaseSelect when parent oscillates bet…
Jun 10, 2026
4e1e1de
fix: keep BaseSelect uncontrolled with layout-effect sync
Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pgk-pr-new.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
run: pnpm install --frozen-lockfile

- name: Build elements 🛠
run: pnpm build
run: pnpm --filter @commercelayer/core --filter @commercelayer/hooks --filter @commercelayer/react-components build

- name: Publish 🚀 pkg.pr.new
run: |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"build": "pnpm -r build",
"build:watch": "pnpm --filter @commercelayer/core --filter @commercelayer/hooks --filter @commercelayer/react-components --parallel build:watch",
"prepare": "husky",
"test": "pnpm -r --workspace-concurrency=1 --no-bail test",
"docs:dev": "pnpm --filter docs storybook",
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"build": "tsup",
"build:watch": "tsup --watch",
"ci": "pnpm build && pnpm check-exports && pnpm lint"
},
"publishConfig": {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/addresses/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./saveOrderAddresses"
export * from "./updateAddressReference"
296 changes: 296 additions & 0 deletions packages/core/src/addresses/saveOrderAddresses.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
import { beforeEach, describe, expect, test, vi } from "vitest"
import { saveOrderAddresses } from "./saveOrderAddresses.js"

const {
mockCreate,
mockUpdate,
mockRelationship,
mockAddRequestInterceptor,
mockAddResponseInterceptor,
mockAddRawResponseReader,
} = vi.hoisted(() => {
const mockCreate = vi.fn()
const mockUpdate = vi.fn()
const mockRelationship = vi.fn((id: string) => ({ id, type: "customer_addresses" }))
const mockAddRequestInterceptor = vi.fn().mockReturnValue(1)
const mockAddResponseInterceptor = vi.fn().mockReturnValue(1)
const mockAddRawResponseReader = vi.fn()
return {
mockCreate,
mockUpdate,
mockRelationship,
mockAddRequestInterceptor,
mockAddResponseInterceptor,
mockAddRawResponseReader,
}
})

vi.mock("@commercelayer/sdk/bundle", () => ({
CommerceLayer: vi.fn().mockReturnValue({
addRequestInterceptor: mockAddRequestInterceptor,
addResponseInterceptor: mockAddResponseInterceptor,
addRawResponseReader: mockAddRawResponseReader,
addresses: {
create: mockCreate,
update: mockUpdate,
relationship: mockRelationship,
},
}),
}))

vi.mock("@commercelayer/js-auth", () => ({
jwtDecode: vi.fn().mockReturnValue({ payload: { organization: { slug: "my-org" } } }),
}))

const baseOrder = { id: "ord_1" }

beforeEach(() => {
vi.clearAllMocks()
mockAddRequestInterceptor.mockReturnValue(1)
mockAddResponseInterceptor.mockReturnValue(1)
mockCreate.mockResolvedValue({ id: "addr_new" })
mockUpdate.mockResolvedValue({ id: "addr_existing" })
mockRelationship.mockImplementation((id: string) => ({ id, type: "addresses" }))
})

describe("saveOrderAddresses", () => {
describe("billing address – create (no existing address)", () => {
test("creates a new address and sets billing_address relationship", async () => {
const result = await saveOrderAddresses({
accessToken: "token",
order: baseOrder,
billingAddress: { first_name: "John", last_name: "Doe" },
})

expect(result.success).toBe(true)
expect(mockCreate).toHaveBeenCalledWith({ first_name: "John", last_name: "Doe" })
expect(mockRelationship).toHaveBeenCalledWith("addr_new")
expect(result.orderAttributes?.billing_address).toEqual({ id: "addr_new", type: "addresses" })
expect(result.orderAttributes?._shipping_address_same_as_billing).toBe(true)
expect(result.orderAttributes?._billing_address_clone_id).toBeUndefined()
expect(result.orderAttributes?._refresh).toBeUndefined()
})

test("creates a new address when existing billing_address has a reference", async () => {
const result = await saveOrderAddresses({
accessToken: "token",
order: {
...baseOrder,
billing_address: { id: "addr_old", reference: "cust-addr-ref" },
},
billingAddress: { first_name: "Jane" },
})

expect(mockCreate).toHaveBeenCalled()
expect(mockUpdate).not.toHaveBeenCalled()
expect(result.orderAttributes?.billing_address).toBeDefined()
expect(result.orderAttributes?._refresh).toBeUndefined()
})
})

describe("billing address – update (existing address without reference)", () => {
test("updates an existing address and sets _refresh=true", async () => {
const result = await saveOrderAddresses({
accessToken: "token",
order: {
...baseOrder,
billing_address: { id: "addr_existing", reference: null },
},
billingAddress: { first_name: "Updated" },
})

expect(result.success).toBe(true)
expect(mockUpdate).toHaveBeenCalledWith({ id: "addr_existing", first_name: "Updated" })
expect(mockCreate).not.toHaveBeenCalled()
expect(result.orderAttributes?._refresh).toBe(true)
expect(result.orderAttributes?.billing_address).toBeUndefined()
})
})

describe("billing address clone ID", () => {
test("sets clone IDs in orderAttributes and skips address creation", async () => {
const result = await saveOrderAddresses({
accessToken: "token",
order: baseOrder,
billingAddressCloneId: "cust_addr_1",
})

expect(result.success).toBe(true)
expect(mockCreate).not.toHaveBeenCalled()
expect(result.orderAttributes?._billing_address_clone_id).toBe("cust_addr_1")
expect(result.orderAttributes?._shipping_address_same_as_billing).toBe(true)
expect(result.orderAttributes?._shipping_address_clone_id).toBe("cust_addr_1")
})

test("reuses billing address ID when billing reference matches clone ID", async () => {
const result = await saveOrderAddresses({
accessToken: "token",
order: {
...baseOrder,
billing_address: { id: "addr_existing", reference: "cust_addr_1" },
shipping_address: { id: "ship_existing", reference: "cust_addr_1" },
},
billingAddressCloneId: "cust_addr_1",
})

expect(result.success).toBe(true)
// Billing clone ID is reused from the existing address ID
expect(result.orderAttributes?._billing_address_clone_id).toBe("addr_existing")
// Shipping clone ID is subsequently set by the !shipToDifferentAddress branch
expect(result.orderAttributes?._shipping_address_same_as_billing).toBe(true)
})
})

describe("shipping address – ship to different address", () => {
test("creates a new shipping address when shipToDifferentAddress=true", async () => {
mockCreate
.mockResolvedValueOnce({ id: "billing_new" })
.mockResolvedValueOnce({ id: "shipping_new" })

const result = await saveOrderAddresses({
accessToken: "token",
order: baseOrder,
billingAddress: { first_name: "Biller" },
shippingAddress: { first_name: "Shipper" },
shipToDifferentAddress: true,
})

expect(result.success).toBe(true)
expect(mockCreate).toHaveBeenCalledTimes(2)
expect(result.orderAttributes?._shipping_address_same_as_billing).toBeUndefined()
expect(result.orderAttributes?.shipping_address).toEqual({ id: "shipping_new", type: "addresses" })
})

test("updates an existing shipping address when shipToDifferentAddress=true", async () => {
const result = await saveOrderAddresses({
accessToken: "token",
order: {
...baseOrder,
shipping_address: { id: "ship_existing", reference: null },
},
shippingAddress: { first_name: "Updated Shipper" },
shipToDifferentAddress: true,
})

expect(result.success).toBe(true)
expect(mockUpdate).toHaveBeenCalledWith({ id: "ship_existing", first_name: "Updated Shipper" })
expect(result.orderAttributes?._refresh).toBe(true)
})

test("sets shipping clone ID when shipToDifferentAddress=true and shippingAddressCloneId is provided", async () => {
const result = await saveOrderAddresses({
accessToken: "token",
order: baseOrder,
shippingAddressCloneId: "cust_ship_1",
shipToDifferentAddress: true,
})

expect(result.success).toBe(true)
expect(result.orderAttributes?._shipping_address_clone_id).toBe("cust_ship_1")
expect(result.orderAttributes?._shipping_address_same_as_billing).toBeUndefined()
})

test("skips shipping when shipToDifferentAddress=false (no clone ID)", async () => {
const result = await saveOrderAddresses({
accessToken: "token",
order: baseOrder,
billingAddress: { first_name: "Biller" },
shipToDifferentAddress: false,
})

expect(result.success).toBe(true)
expect(result.orderAttributes?._shipping_address_same_as_billing).toBe(true)
expect(mockCreate).toHaveBeenCalledTimes(1) // only billing
})
})

describe("metadata sanitization", () => {
test("moves metadata_ prefixed keys into metadata object", async () => {
await saveOrderAddresses({
accessToken: "token",
order: baseOrder,
billingAddress: {
first_name: "John",
metadata_custom_field: "value",
},
})

expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
first_name: "John",
metadata: { custom_field: "value" },
})
)
expect(mockCreate).not.toHaveBeenCalledWith(
expect.objectContaining({ metadata_custom_field: expect.anything() })
)
})
})

describe("customer email", () => {
test("includes customer email in orderAttributes", async () => {
const result = await saveOrderAddresses({
accessToken: "token",
order: baseOrder,
customerEmail: "test@example.com",
})

expect(result.success).toBe(true)
expect(result.orderAttributes?.customer_email).toBe("test@example.com")
})
})

describe("empty inputs", () => {
test("returns success with minimal orderAttributes when no address data is provided", async () => {
const result = await saveOrderAddresses({
accessToken: "token",
order: baseOrder,
})

expect(result.success).toBe(true)
expect(mockCreate).not.toHaveBeenCalled()
expect(mockUpdate).not.toHaveBeenCalled()
expect(result.orderAttributes?.id).toBe("ord_1")
})

test("ignores empty billingAddress object", async () => {
const result = await saveOrderAddresses({
accessToken: "token",
order: baseOrder,
billingAddress: {},
})

expect(result.success).toBe(true)
expect(mockCreate).not.toHaveBeenCalled()
})

test("ignores empty shippingAddress when shipToDifferentAddress=true", async () => {
const result = await saveOrderAddresses({
accessToken: "token",
order: baseOrder,
shippingAddress: {},
shipToDifferentAddress: true,
})

expect(result.success).toBe(true)
expect(mockCreate).not.toHaveBeenCalled()
})
})

describe("error handling", () => {
test("returns success=false and error when SDK throws", async () => {
const sdkError = new Error("Network error")
mockCreate.mockRejectedValueOnce(sdkError)

const result = await saveOrderAddresses({
accessToken: "token",
order: baseOrder,
billingAddress: { first_name: "John" },
})

expect(result.success).toBe(false)
expect(result.error).toBe(sdkError)
expect(result.orderAttributes).toBeUndefined()
})
})
})
Loading
Loading