@evenicanpm/storefront-core 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/package.json +9 -4
  2. package/src/api-manager/README.md +52 -27
  3. package/src/api-manager/datasources/d365/d365-cart.datasource.ts +166 -17
  4. package/src/api-manager/datasources/d365/d365-invoice.datasource.ts +100 -0
  5. package/src/api-manager/datasources/d365/d365-order.datasource.ts +75 -18
  6. package/src/api-manager/datasources/d365/d365-product.datasource.ts +19 -1
  7. package/src/api-manager/datasources/d365/d365-user.datasource.ts +96 -22
  8. package/src/api-manager/datasources/d365/d365.datasource.ts +3 -0
  9. package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +14 -5
  10. package/src/api-manager/datasources/e4/e4.datasource.ts +9 -0
  11. package/src/api-manager/datasources/e4/middleware.ts +35 -0
  12. package/src/api-manager/datasources/e4/order/e4-order.datasource.ts +7 -4
  13. package/src/api-manager/datasources/e4/user/e4-user.datasource.ts +4 -0
  14. package/src/api-manager/index.ts +54 -3
  15. package/src/api-manager/lib/get-graphql-client.ts +13 -6
  16. package/src/api-manager/schemas/cart.schema.ts +10 -1
  17. package/src/api-manager/schemas/invoice.schema.ts +55 -0
  18. package/src/api-manager/schemas/order.schema.ts +13 -1
  19. package/src/api-manager/schemas/user.schema.ts +8 -0
  20. package/src/api-manager/services/create-query.ts +5 -56
  21. package/src/api-manager/services/invoice/queries/get-invoice-details.ts +18 -0
  22. package/src/api-manager/services/invoice/queries/get-invoices.ts +18 -0
  23. package/src/api-manager/services/order/queries/get-order-details.ts +1 -1
  24. package/src/api-manager/services/order/queries/get-orders.ts +6 -3
  25. package/src/api-manager/services/user/queries/get-customer-balance.ts +20 -0
  26. package/src/api-manager/types/Datasource.ts +21 -2
  27. package/src/auth/better-auth.ts +89 -0
  28. package/src/auth/signout.ts +5 -34
  29. package/src/cms/blocks/block-manager.tsx +18 -3
  30. package/src/cms/blocks/components/cta-banner.tsx +65 -0
  31. package/src/cms/blocks/components/hero-image.tsx +83 -0
  32. package/src/cms/blocks/index.tsx +2 -0
  33. package/src/cms/blocks/interfaces.ts +20 -0
  34. package/src/cms/draft-mode-badge.tsx +26 -0
  35. package/src/cms/queries.ts +81 -0
  36. package/src/components/flex-box/flex-box.tsx +2 -0
  37. package/src/components/header/__tests__/header.test.tsx +9 -2
  38. package/src/components/header/__tests__/user.test.tsx +34 -19
  39. package/src/components/header/components/user.tsx +70 -29
  40. package/src/components/mini-cart/mini-cart.tsx +3 -3
  41. package/src/components/wishlist-dialogs/add-to-wishlist/compound/add-to-wishlist-dialog.tsx +3 -1
  42. package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +4 -1
  43. package/src/hooks/use-nextauth-session.ts +10 -14
  44. package/src/pages/account/account-navigation.tsx +88 -60
  45. package/src/pages/account/account-routes.ts +52 -0
  46. package/src/pages/account/orders/order-history-filter-types.tsx +26 -0
  47. package/src/pages/account/orders/order-history-filters-panel.tsx +375 -0
  48. package/src/pages/account/orders/order-history-header.tsx +54 -0
  49. package/src/pages/account/orders/order-history-pagination.tsx +55 -0
  50. package/src/pages/account/orders/order-history-root.tsx +125 -0
  51. package/src/pages/account/orders/order-history-row.tsx +110 -0
  52. package/src/pages/account/orders/order-history-search-and-filter.tsx +449 -0
  53. package/src/pages/account/orders/order-history-search-bar.tsx +84 -0
  54. package/src/pages/account/orders/order-history-search-dropdown.tsx +53 -0
  55. package/src/pages/account/orders/order-history-sort.tsx +90 -0
  56. package/src/pages/account/orders/order-history-table.tsx +54 -0
  57. package/src/pages/account/orders/order-row.tsx +2 -2
  58. package/src/pages/account/orders/orders-drop-down-handler.tsx +19 -0
  59. package/src/pages/account/table-header.tsx +20 -0
  60. package/src/pages/account/wishlist/wishlist-item.tsx +1 -1
  61. package/src/pages/blog/blog-card.tsx +9 -3
  62. package/src/pages/blog/blog-detail-view.tsx +150 -0
  63. package/src/pages/blog/blog-list-view.tsx +59 -0
  64. package/src/pages/cart/__tests__/checkout-form.test.tsx +4 -2
  65. package/src/pages/cart/estimate-shipping.tsx +6 -5
  66. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +43 -21
  67. package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +144 -19
  68. package/src/pages/cms-page-view.tsx +53 -0
  69. package/src/pages/quickorder/order-upload.tsx +12 -6
  70. package/src/pages/quickorder/quick-order.tsx +25 -20
  71. package/src/api-manager/datasources/d365/utils/decode-jwt.ts +0 -10
  72. package/src/auth/auth-options.test.ts +0 -70
  73. package/src/auth/auth-options.ts +0 -101
  74. package/src/auth/next-auth-cookie-manager.ts +0 -87
  75. package/src/auth/providers/aadb2c-provider.ts +0 -32
  76. package/src/auth/providers/authentik-provider.ts +0 -24
  77. package/src/auth/providers/entra-external-id-provider.ts +0 -50
  78. package/src/auth/providers/index.ts +0 -16
  79. package/src/auth/providers/keycloak-provider.ts +0 -22
  80. package/src/auth/refresh-token.ts +0 -132
  81. package/src/auth/types/next-auth.d.ts +0 -38
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evenicanpm/storefront-core",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Core module for D365/e4 Headless Storefront",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -16,7 +16,9 @@
16
16
  "author": "Evenica",
17
17
  "license": "ISC",
18
18
  "dependencies": {
19
- "@evenicanpm/ui": "^2.0.0"
19
+ "@evenicanpm/cms": "^2.3.0",
20
+ "@evenicanpm/ui": "^2.0.0",
21
+ "strapi-sdk-js": "^3.0.0"
20
22
  },
21
23
  "devDependencies": {
22
24
  "@storybook/addon-docs": "^9.1.6",
@@ -31,14 +33,17 @@
31
33
  },
32
34
  "peerDependencies": {
33
35
  "@msdyn365-commerce/retail-proxy": "9.50",
36
+ "@opentelemetry/api": "^1.9.1",
37
+ "@strapi/blocks-react-renderer": "^1.0.1",
34
38
  "@tanstack/react-query": "^5.76.1",
39
+ "better-auth": "*",
35
40
  "graphql": "^16.11.0",
36
41
  "graphql-request": "^7.1.2",
37
42
  "jsonwebtoken": "^9.0.2",
38
43
  "lodash": "^4.17.21",
39
44
  "next": "^15.2.1",
40
- "next-auth": "^4.24.11",
41
45
  "object-mapper": "^6.2.0",
46
+ "set-cookie-parser": "^2.7.1",
42
47
  "typedoc": "^0.28.9",
43
48
  "typedoc-plugin-merge-modules": "^7.0.0",
44
49
  "typescript": "^5.8.3",
@@ -49,5 +54,5 @@
49
54
  "esbuild": "0.19.11"
50
55
  }
51
56
  },
52
- "gitHead": "8227589295d3d63586f9b26fff8d643b070989b3"
57
+ "gitHead": "72bf1aeb02a3d96c587706c0d91606698e38ff6a"
53
58
  }
@@ -1,46 +1,71 @@
1
1
  # Api Manager
2
2
 
3
- - All data fetching will originate from this layer
3
+ All data fetching originates from this layer. `createApiManager()` selects the active datasource and applies any project-level overrides before returning it.
4
4
 
5
- ## User Guide
5
+ ## Selecting a Primary Datasource
6
6
 
7
- ### 1. Setup the Api Manager config file
7
+ Set the `API_MANAGER_PRIMARY` environment variable to control which datasource is used. Defaults to `"e4"` when unset.
8
8
 
9
- 1. Add a `@api-manager-config` webpack alias in your storefront next.config.js
10
-
11
- ```js
12
- "@api-manager-config": path.resolve(
13
- process.cwd(),
14
- "api-manager.config.ts",
15
- )
9
+ ```env
10
+ API_MANAGER_PRIMARY=e4 # default
11
+ API_MANAGER_PRIMARY=d365
16
12
  ```
17
13
 
18
- 2. Create a JS file that exports the config object as the default.
19
- 3. Storefront-core will read this alias when creating api manager.
14
+ ## Project-Level Overrides
15
+
16
+ Projects can override individual methods on any datasource entity without replacing the entire datasource. This allows mixing custom logic with the default implementation.
17
+
18
+ ### How it works
19
+
20
+ `createApiManager()` loads `src/extensions/api-manager-overrides/overrides.datasource.ts` via webpack's `require.context` (using the `@storefront` alias). Any methods exported from that file are merged on top of the selected datasource's entity methods.
21
+
22
+ ### Setup
23
+
24
+ Create or edit `src/extensions/api-manager-overrides/overrides.datasource.ts` in your storefront project:
20
25
 
21
- ### 2. Add a new method to the `ApiManager`
26
+ ```ts
27
+ import type { SecondaryDatasource } from "@evenicanpm/storefront-core/src/api-manager/types/Datasource";
22
28
 
23
- 1. If the entity does not have a `entity.model.ts` file in `src/app/data-access/models`, add one and create schemas for input types eg. `addToCartInput`
24
- 2. Add usecase to `data-access/Datasource.ts` interface
25
- 3. Add the same usecase to `data-access/ApiManager.ts` class methods
26
- 4. Finally, add method to the relevant data source, eg. `/src/app/data-access/datasources/E4.datasource.ts`
27
- 5. You are now able to call the method from the `ApiManager.`
29
+ const Overrides: SecondaryDatasource = {
30
+ categories: {
31
+ getCategories: async () => {
32
+ // custom implementation replaces only this method
33
+ return myCustomCategories();
34
+ },
35
+ },
36
+ };
37
+
38
+ export default Overrides;
39
+ ```
40
+
41
+ ### Rules
42
+
43
+ - Only the methods you define in `Overrides` are replaced. All other methods continue to use the primary datasource.
44
+ - You can override methods on any core entity: `user`, `session`, `cart`, `product`, `categories`, `order`, `address`, `organization`, `invoice`.
45
+ - You can also add entirely new non-core entities by including them in the `Overrides` object.
46
+ - The file must use `export default` — named exports are ignored.
28
47
 
29
48
  ## Folder Structure
30
49
 
31
- ### Datasources
50
+ ### `datasources/`
51
+
52
+ Implementations for each supported backend (`e4`, `d365`).
53
+
54
+ ### `types/`
32
55
 
33
- - External API's
56
+ - `Datasource` — abstract base class; defines the shape all datasources must implement.
57
+ - `PrimaryDatasource` — alias for `Datasource`; all methods required.
58
+ - `SecondaryDatasource` — `Partial<Datasource>`; use this type for override objects.
59
+ - `DatasourceApis` — interface defining all entity API contracts.
34
60
 
35
- ### Schemas
61
+ ### `schemas/`
36
62
 
37
- - input/output types for all datasource
38
- - D365 is base type, all other datasources should translate to this model
63
+ Input/output types for all datasource methods. D365 types are the canonical model; other datasources translate to this shape.
39
64
 
40
- ### Lib
65
+ ### `lib/`
41
66
 
42
- - Utilities and helpers that instantiate api manager etc.
67
+ Utilities and helpers (e.g. category tree builders).
43
68
 
44
- ### Services
69
+ ### `services/`
45
70
 
46
- - React-query **cache** layer
71
+ React-query cache layer — sits above the api-manager.
@@ -1,5 +1,35 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
 
3
+ import { SpanStatusCode, trace } from "@opentelemetry/api";
4
+
5
+ function serializeError(error: unknown): string {
6
+ if (error instanceof Error) return error.message;
7
+ if (typeof error === "string") return error;
8
+ try {
9
+ return JSON.stringify(error, (_key, value) => {
10
+ if (value === null || typeof value !== "object") return value;
11
+ const allKeys = new Set<string>();
12
+ let proto = value;
13
+ while (proto && proto !== Object.prototype) {
14
+ Object.getOwnPropertyNames(proto).forEach((k) => allKeys.add(k));
15
+ proto = Object.getPrototypeOf(proto);
16
+ }
17
+ Object.keys(value).forEach((k) => allKeys.add(k));
18
+ const result: Record<string, unknown> = {};
19
+ for (const key of allKeys) {
20
+ if (key !== "constructor") {
21
+ try {
22
+ result[key] = (value as any)[key];
23
+ } catch {}
24
+ }
25
+ }
26
+ return Object.keys(result).length ? result : value;
27
+ });
28
+ } catch {
29
+ return String(error);
30
+ }
31
+ }
32
+
3
33
  import { D365DatasourceBase } from "@evenicanpm/storefront-core/src/api-manager/datasources/d365/d365-base.datasource";
4
34
  import D365OrganizationDatasource from "@evenicanpm/storefront-core/src/api-manager/datasources/d365/d365-organization.datasource";
5
35
  import { D365ProductDatasource } from "@evenicanpm/storefront-core/src/api-manager/datasources/d365/d365-product.datasource";
@@ -24,6 +54,7 @@ import {
24
54
  import {
25
55
  addCartLinesAsync,
26
56
  addDiscountCodeAsync,
57
+ addInvoicesAsync,
27
58
  checkoutAsync,
28
59
  createCartAsync,
29
60
  getCardPaymentAcceptPointAsync,
@@ -32,6 +63,7 @@ import {
32
63
  removeDiscountCodesAsync,
33
64
  retrieveCardPaymentAcceptResultAsync,
34
65
  searchAsync,
66
+ setInvoiceLinePriceAsync,
35
67
  updateAsync,
36
68
  updateCartLinesAsync,
37
69
  updateLineDeliverySpecificationsAsync,
@@ -54,28 +86,76 @@ import type {
54
86
  UpdateCartInput,
55
87
  UpdateCartLineDeliveryModeInput,
56
88
  UpdateCartLineInput,
89
+ UpdateInvoiceLineInput,
57
90
  } from "@/schemas/cart.schema";
58
91
  import type { SalesOrder } from "@/schemas/order.schema";
59
92
 
60
93
  export class D365CartDatasource extends D365DatasourceBase implements CartApi {
61
94
  async checkout(input: CheckoutInput): Promise<SalesOrder> {
62
- const context = this.context;
63
- const { currency, amount, tokenizedPaymentCard } = input;
64
- const cartTenderLine: CartTenderLine = {
65
- Currency: currency,
66
- Amount: amount,
67
- TenderTypeId: tokenizedPaymentCard.TenderType,
68
- CardTypeId: tokenizedPaymentCard.CardTypeId,
69
- TokenizedPaymentCard: tokenizedPaymentCard,
70
- };
71
- return await checkoutAsync(
72
- context as any,
73
- input.id,
74
- input.receiptEmail,
75
- undefined,
76
- undefined,
77
- [cartTenderLine],
78
- );
95
+ const tracer = trace.getTracer("d365-cart");
96
+ return tracer.startActiveSpan("d365.cart.checkout", async (span) => {
97
+ span.setAttributes({
98
+ "cart.id": input.id,
99
+ "cart.receipt_email": input.receiptEmail ?? "",
100
+ "cart.currency": input.currency,
101
+ "cart.amount": input.amount,
102
+ "cart.payment_method": input.paymentMethod ?? "card",
103
+ "cart.tender_type":
104
+ input.tokenizedPaymentCard?.TenderType ?? "on-account",
105
+ });
106
+ try {
107
+ const context = this.context;
108
+ const { currency, amount, paymentMethod } = input;
109
+
110
+ let cartTenderLines: CartTenderLine[];
111
+ if (paymentMethod === "on-account") {
112
+ cartTenderLines = [
113
+ {
114
+ Currency: currency,
115
+ Amount: amount,
116
+ TenderTypeId: "4",
117
+ } as CartTenderLine,
118
+ ];
119
+ } else {
120
+ const { tokenizedPaymentCard } = input;
121
+ if (!tokenizedPaymentCard) {
122
+ throw new Error(
123
+ "tokenizedPaymentCard is required for card payments",
124
+ );
125
+ }
126
+ cartTenderLines = [
127
+ {
128
+ Currency: currency,
129
+ Amount: amount,
130
+ TenderTypeId: tokenizedPaymentCard.TenderType,
131
+ CardTypeId: tokenizedPaymentCard.CardTypeId,
132
+ TokenizedPaymentCard: tokenizedPaymentCard,
133
+ },
134
+ ];
135
+ }
136
+
137
+ const result = await checkoutAsync(
138
+ context as any,
139
+ input.id,
140
+ input.receiptEmail,
141
+ undefined,
142
+ undefined,
143
+ cartTenderLines,
144
+ );
145
+ span.setStatus({ code: SpanStatusCode.OK });
146
+ return result;
147
+ } catch (error) {
148
+ const message = serializeError(error);
149
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
150
+ span.recordException(
151
+ error instanceof Error ? error : new Error(message),
152
+ );
153
+ span.setAttributes({ "error.message": message });
154
+ throw error;
155
+ } finally {
156
+ span.end();
157
+ }
158
+ });
79
159
  }
80
160
  async retrieveCardPaymentAcceptResult(
81
161
  input: RetrieveCardPaymentAcceptResultInput,
@@ -440,6 +520,75 @@ export class D365CartDatasource extends D365DatasourceBase implements CartApi {
440
520
  }
441
521
  return {};
442
522
  }
523
+
524
+ async addInvoicesToCart(
525
+ cart: Readonly<Cart>,
526
+ invoiceIds: string[],
527
+ ): Promise<Cart> {
528
+ const context = this.context;
529
+
530
+ if (!cart.Version || !cart.Id) {
531
+ throw new Error("CartId and Version are required to add a cart line");
532
+ }
533
+
534
+ if (cart.Version) {
535
+ try {
536
+ const updatedCart = await addInvoicesAsync(
537
+ context as any,
538
+ cart.Id,
539
+ invoiceIds,
540
+ );
541
+
542
+ return updatedCart as Cart;
543
+ } catch (error) {
544
+ console.error("Error adding invoices to cart:", {
545
+ cartId: cart.Id,
546
+ invoiceIds,
547
+ error,
548
+ });
549
+ throw new Error(
550
+ `Unable to add invoices to cart: ${JSON.stringify(error)}`,
551
+ );
552
+ }
553
+ }
554
+ throw new Error("Cart version is required to add invoices to cart");
555
+ }
556
+
557
+ async setInvoiceLinePrice(input: UpdateInvoiceLineInput): Promise<Cart> {
558
+ const context = this.context;
559
+
560
+ if (!input.cartId || !input.cartLineId) {
561
+ throw new Error(
562
+ "CartId and CartLineId are required to set invoice line price",
563
+ );
564
+ }
565
+
566
+ if (input.cartId) {
567
+ try {
568
+ const updatedCart = await setInvoiceLinePriceAsync(
569
+ context as any,
570
+ input.cartId,
571
+ input.cartLineId,
572
+ input.price,
573
+ input.version,
574
+ );
575
+ return updatedCart as Cart;
576
+ } catch (error) {
577
+ console.error("Error setting invoice line price:", {
578
+ cartId: input.cartId,
579
+ cartLineId: input.cartLineId,
580
+ price: input.price,
581
+ error,
582
+ });
583
+ throw new Error(
584
+ `Unable to set invoice line price: ${JSON.stringify(error)}`,
585
+ );
586
+ }
587
+ }
588
+ throw new Error(
589
+ "CartId and CartLineId are required to set invoice line price",
590
+ );
591
+ }
443
592
  }
444
593
 
445
594
  const mergeCartWithProducts = async (
@@ -0,0 +1,100 @@
1
+ import { D365DatasourceBase } from "@evenicanpm/storefront-core/src/api-manager/datasources/d365/d365-base.datasource";
2
+ import type {
3
+ GetInvoicesDetailsInput,
4
+ GetInvoicesInput,
5
+ Invoice,
6
+ InvoiceDetails,
7
+ InvoiceDetailsSearchCriteria,
8
+ InvoiceSearchCriteria,
9
+ } from "@evenicanpm/storefront-core/src/api-manager/schemas/invoice.schema";
10
+ import type { InvoiceApi } from "@evenicanpm/storefront-core/src/api-manager/types/Datasource";
11
+ import {
12
+ getInvoiceDetailsAsync,
13
+ getInvoicesAsync,
14
+ } from "@msdyn365-commerce/retail-proxy/dist/DataActions/SalesOrdersDataActions.g";
15
+ import type { IContext } from "@msdyn365-commerce/retail-proxy/dist/Interfaces/ICallerContext";
16
+ import { SpanStatusCode, trace } from "@opentelemetry/api";
17
+
18
+ const tracer = trace.getTracer("d365-invoice-datasource");
19
+
20
+ /**
21
+ * Datasource for fetching invoice data from Dynamics 365. Implements the InvoiceApi interface.
22
+ * Uses the retail proxy's data actions to retrieve invoice information based on search criteria.
23
+ * Handles errors gracefully and logs them for debugging purposes.
24
+ */
25
+ class D365InvoiceDatasource extends D365DatasourceBase implements InvoiceApi {
26
+ /**
27
+ * Fetches a list of invoices based on the provided search criteria. Supports filtering by invoice IDs, customer ID, business partner ID, invoice type, and paid status.
28
+ * Utilizes OpenTelemetry for tracing the execution of the method, including logging attributes and handling errors.
29
+ * @param input The input containing the search criteria for fetching invoices.
30
+ * @returns A promise that resolves to an array of invoices matching the search criteria.
31
+ */
32
+ async getInvoices(input: GetInvoicesInput): Promise<Invoice[]> {
33
+ return tracer.startActiveSpan("d365.getInvoices", async (span) => {
34
+ try {
35
+ const context = this.context;
36
+
37
+ const invoiceSearchCriteria: InvoiceSearchCriteria = {
38
+ InvoiceIds: input.InvoiceSearchCriteria.InvoiceIds,
39
+ CustomerId: input.InvoiceSearchCriteria.CustomerId,
40
+ BusinessPartnerId: input.InvoiceSearchCriteria.BusinessPartnerId,
41
+ InvoiceTypeValues: input.InvoiceSearchCriteria.InvoiceTypeValues,
42
+ InvoicePaidStatusValues:
43
+ input.InvoiceSearchCriteria.InvoicePaidStatusValues,
44
+ };
45
+ span.setAttributes({
46
+ "d365.invoice.customer_id":
47
+ input.InvoiceSearchCriteria.CustomerId ?? "",
48
+ "d365.invoice.business_partner_id":
49
+ input.InvoiceSearchCriteria.BusinessPartnerId ?? "",
50
+ "d365.invoice.paid_status_filter": JSON.stringify(
51
+ input.InvoiceSearchCriteria.InvoicePaidStatusValues ?? [],
52
+ ),
53
+ });
54
+
55
+ const invoices = await getInvoicesAsync(
56
+ context as unknown as IContext,
57
+ invoiceSearchCriteria,
58
+ );
59
+ // Retail proxy returns ObservableArray — normalize to plain array
60
+ const result = Array.from(invoices ?? []) as Invoice[];
61
+
62
+ span.setAttributes({ "d365.invoice.result_count": result.length });
63
+ span.setStatus({ code: SpanStatusCode.OK });
64
+ return result;
65
+ } catch (error) {
66
+ const message =
67
+ error instanceof Error ? error.message : JSON.stringify(error);
68
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
69
+ span.recordException(
70
+ error instanceof Error ? error : new Error(message),
71
+ );
72
+ throw new Error(`getInvoices failed: ${message}`);
73
+ } finally {
74
+ span.end();
75
+ }
76
+ });
77
+ }
78
+
79
+ async getInvoiceDetails(
80
+ input: GetInvoicesDetailsInput,
81
+ ): Promise<InvoiceDetails> {
82
+ try {
83
+ const context = this.context;
84
+ const invoiceSearchCriteria: InvoiceDetailsSearchCriteria = {
85
+ InvoiceId: input.InvoiceSearchCriteria.InvoiceId,
86
+ };
87
+ const invoices = await getInvoiceDetailsAsync(
88
+ context as unknown as IContext,
89
+ invoiceSearchCriteria,
90
+ );
91
+
92
+ return invoices;
93
+ } catch (error) {
94
+ console.error("Error fetching invoice details:", error);
95
+ throw error;
96
+ }
97
+ }
98
+ }
99
+
100
+ export default D365InvoiceDatasource;
@@ -1,42 +1,99 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
1
3
  import { D365DatasourceBase } from "@evenicanpm/storefront-core/src/api-manager/datasources/d365/d365-base.datasource";
2
4
  import { D365ProductDatasource } from "@evenicanpm/storefront-core/src/api-manager/datasources/d365/d365-product.datasource";
3
5
  import type {
4
6
  GetSalesOrderDetailsInput,
7
+ GetSalesOrdersInput,
5
8
  SalesOrder,
6
- SalesOrderInput,
7
9
  } from "@evenicanpm/storefront-core/src/api-manager/schemas/order.schema";
8
10
  import type { OrderApi } from "@evenicanpm/storefront-core/src/api-manager/types/Datasource";
9
11
  import {
12
+ CustomerOrderType,
10
13
  type SalesOrder as DynSalesOrder,
11
14
  type ICallerContext,
15
+ type OrderSearchCriteria,
16
+ type QueryResultSettings,
12
17
  SearchLocation,
13
18
  } from "@msdyn365-commerce/retail-proxy";
14
- import { getSalesOrderDetailsByTransactionIdAsync } from "@msdyn365-commerce/retail-proxy/dist/DataActions/SalesOrdersDataActions.g";
15
- import type { IContext } from "@msdyn365-commerce/retail-proxy/dist/Interfaces/ICallerContext";
19
+ import { readAsync } from "@msdyn365-commerce/retail-proxy/dist/DataActions/CustomersDataActions.g";
20
+ import {
21
+ getSalesOrderDetailsBySalesIdAsync,
22
+ getSalesOrderDetailsByTransactionIdAsync,
23
+ searchOrdersAsync,
24
+ } from "@msdyn365-commerce/retail-proxy/dist/DataActions/SalesOrdersDataActions.g";
16
25
  import uniq from "lodash/uniq";
26
+ import { getContextFromCookie } from "./utils/get-context-cookie";
17
27
 
18
28
  class D365OrderDatasource extends D365DatasourceBase implements OrderApi {
19
- getSalesOrders(_input: SalesOrderInput): Promise<SalesOrder[]> {
20
- throw new Error("Method not implemented.");
21
- }
22
- async getSalesOrderDetails(
23
- input: GetSalesOrderDetailsInput,
24
- ): Promise<SalesOrder> {
25
- const context = this.context;
26
- const { transactionId } = input;
29
+ getSalesOrders = async (
30
+ input: GetSalesOrdersInput,
31
+ ): Promise<SalesOrder[]> => {
32
+ const context = await getContextFromCookie();
33
+ const customer = await readAsync(context as any, "");
34
+
35
+ const searchCriteria: OrderSearchCriteria = {
36
+ ...input.orderSearchCriteria,
37
+ OrderType: CustomerOrderType.SalesOrder,
38
+ CustomerAccountNumber: customer.AccountNumber,
39
+ };
40
+
41
+ const queryResultSettings: QueryResultSettings = {};
42
+ if (input.paging) queryResultSettings.Paging = input.paging;
43
+ if (input.sort)
44
+ queryResultSettings.Sorting = {
45
+ Columns: [input.sort],
46
+ };
47
+
48
+ (context as any).queryResultSettings = queryResultSettings;
49
+
27
50
  try {
28
- const data = await getSalesOrderDetailsByTransactionIdAsync(
29
- context as unknown as IContext,
30
- transactionId,
31
- SearchLocation.All,
51
+ const searchOrdersResults = await searchOrdersAsync(
52
+ context as any,
53
+ searchCriteria,
54
+ null,
32
55
  );
33
- const mergedData = await mergeOrderWithProducts(data, this.context);
34
- return mergedData;
56
+
57
+ return searchOrdersResults;
35
58
  } catch (error) {
36
59
  console.error(error);
37
60
  throw error;
38
61
  }
39
- }
62
+ };
63
+
64
+ getSalesOrderDetails = async (
65
+ input: GetSalesOrderDetailsInput,
66
+ ): Promise<SalesOrder> => {
67
+ const context = await getContextFromCookie();
68
+ const { transactionId, salesId } = input;
69
+ if (!transactionId && !salesId) throw new Error("No id provided");
70
+
71
+ let data: DynSalesOrder;
72
+ if (transactionId) {
73
+ try {
74
+ data = await getSalesOrderDetailsByTransactionIdAsync(
75
+ context as any,
76
+ transactionId,
77
+ SearchLocation.All,
78
+ );
79
+ } catch (error) {
80
+ console.error(error);
81
+ throw error;
82
+ }
83
+ } else {
84
+ try {
85
+ data = await getSalesOrderDetailsBySalesIdAsync(
86
+ context as any,
87
+ salesId,
88
+ );
89
+ } catch (error) {
90
+ console.error(error);
91
+ throw error;
92
+ }
93
+ }
94
+ const mergedData = await mergeOrderWithProducts(data, context);
95
+ return mergedData;
96
+ };
40
97
  }
41
98
 
42
99
  const mergeOrderWithProducts = async (
@@ -234,9 +234,27 @@ class D365ProductDatasource extends D365DatasourceBase implements ProductApi {
234
234
  }));
235
235
  }
236
236
  const observableResults = await searchByCriteriaAsync(ctx, searchCriteria);
237
- const results: ProductSearchResult[] = Array.from(
237
+ let results: ProductSearchResult[] = Array.from(
238
238
  observableResults as Iterable<ProductSearchResult>,
239
239
  );
240
+
241
+ // If no results found by ItemId and the values are numeric, retry by product RecordId
242
+ if (!results.length && input.itemIds?.length) {
243
+ const numericIds = input.itemIds
244
+ .filter((id) => /^\d+$/.test(id))
245
+ .map(Number);
246
+ if (numericIds.length) {
247
+ const idSearchCriteria: ProductSearchCriteria = {
248
+ Context: {
249
+ CatalogId: 0,
250
+ ChannelId: context.requestContext.apiSettings.channelId,
251
+ },
252
+ Ids: numericIds,
253
+ };
254
+ const idResults = await searchByCriteriaAsync(ctx, idSearchCriteria);
255
+ results = Array.from(idResults as Iterable<ProductSearchResult>);
256
+ }
257
+ }
240
258
  const promises = [];
241
259
  for (const product of results) {
242
260
  promises.push(