@evenicanpm/storefront-core 2.2.0 → 2.3.1
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.
- package/package.json +7 -3
- package/src/api-manager/README.md +52 -27
- package/src/api-manager/datasources/d365/d365-cart.datasource.ts +166 -17
- package/src/api-manager/datasources/d365/d365-invoice.datasource.ts +100 -0
- package/src/api-manager/datasources/d365/d365-order.datasource.ts +75 -18
- package/src/api-manager/datasources/d365/d365-product.datasource.ts +19 -1
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +125 -36
- package/src/api-manager/datasources/d365/d365.datasource.ts +3 -0
- package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +10 -3
- package/src/api-manager/datasources/e4/e4.datasource.ts +9 -0
- package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +7 -0
- package/src/api-manager/datasources/e4/order/e4-order.datasource.ts +7 -4
- package/src/api-manager/datasources/e4/user/e4-user.datasource.ts +4 -0
- package/src/api-manager/index.ts +54 -3
- package/src/api-manager/lib/get-graphql-client.ts +11 -5
- package/src/api-manager/schemas/cart.schema.ts +10 -1
- package/src/api-manager/schemas/invoice.schema.ts +55 -0
- package/src/api-manager/schemas/order.schema.ts +13 -1
- package/src/api-manager/schemas/user.schema.ts +8 -0
- package/src/api-manager/services/create-query.ts +4 -10
- package/src/api-manager/services/invoice/queries/get-invoice-details.ts +18 -0
- package/src/api-manager/services/invoice/queries/get-invoices.ts +18 -0
- package/src/api-manager/services/order/queries/get-order-details.ts +1 -1
- package/src/api-manager/services/order/queries/get-orders.ts +6 -3
- package/src/api-manager/services/user/queries/get-customer-balance.ts +20 -0
- package/src/api-manager/types/Datasource.ts +21 -2
- package/src/auth/better-auth.ts +1 -1
- package/src/cms/blocks/block-manager.tsx +18 -3
- package/src/cms/blocks/components/cta-banner.tsx +65 -0
- package/src/cms/blocks/components/footer/footer-content.tsx +61 -0
- package/src/cms/blocks/components/footer/index.tsx +9 -71
- package/src/cms/blocks/components/footer/interfaces.ts +4 -0
- package/src/cms/blocks/components/footer/sections/footer-contact.tsx +18 -9
- package/src/cms/blocks/components/footer/sections/footer-logo.tsx +17 -2
- package/src/cms/blocks/components/hero-image.tsx +83 -0
- package/src/cms/blocks/index.tsx +2 -0
- package/src/cms/blocks/interfaces.ts +20 -0
- package/src/cms/draft-mode-badge.tsx +26 -0
- package/src/cms/queries.ts +81 -0
- package/src/components/_tests_/site-logo.test.tsx +1 -1
- package/src/components/flex-box/flex-box.tsx +2 -0
- package/src/components/header/components/user.tsx +35 -11
- package/src/components/header/sticky-header.tsx +1 -0
- package/src/components/mini-cart/mini-cart.tsx +3 -3
- package/src/components/product-cards/product-card-plp/product-card.tsx +4 -12
- package/src/components/product-cards/product-card-plp/styles/index.ts +15 -0
- package/src/components/site-logo.tsx +1 -1
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +1 -0
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-item.tsx +3 -1
- package/src/pages/account/account-navigation.tsx +88 -60
- package/src/pages/account/account-routes.ts +86 -0
- package/src/pages/account/orders/order-history-filter-types.tsx +26 -0
- package/src/pages/account/orders/order-history-filters-panel.tsx +375 -0
- package/src/pages/account/orders/order-history-header.tsx +54 -0
- package/src/pages/account/orders/order-history-pagination.tsx +55 -0
- package/src/pages/account/orders/order-history-root.tsx +125 -0
- package/src/pages/account/orders/order-history-row.tsx +110 -0
- package/src/pages/account/orders/order-history-search-and-filter.tsx +449 -0
- package/src/pages/account/orders/order-history-search-bar.tsx +84 -0
- package/src/pages/account/orders/order-history-search-dropdown.tsx +53 -0
- package/src/pages/account/orders/order-history-sort.tsx +90 -0
- package/src/pages/account/orders/order-history-table.tsx +54 -0
- package/src/pages/account/orders/order-row.tsx +2 -2
- package/src/pages/account/orders/orders-drop-down-handler.tsx +19 -0
- package/src/pages/account/table-header.tsx +20 -0
- package/src/pages/account/table-row-skeleton.tsx +31 -0
- package/src/pages/account/wishlist/wishlist-item.tsx +4 -1
- package/src/pages/blog/blog-card.tsx +9 -3
- package/src/pages/blog/blog-detail-view.tsx +152 -0
- package/src/pages/blog/blog-list-view.tsx +59 -0
- package/src/pages/cart/estimate-shipping.tsx +6 -5
- package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +43 -21
- package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +144 -19
- package/src/pages/cms-page-view.tsx +53 -0
- package/src/pages/product-details/product-description.tsx +25 -13
- package/src/pages/product-details/product-intro/compound/product-intro-images.tsx +26 -8
- package/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton.tsx +17 -21
- package/src/pages/product-details/product-tabs.tsx +20 -4
- package/src/pages/quickorder/order-upload.tsx +12 -6
- package/src/pages/quickorder/quick-order.tsx +25 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evenicanpm/storefront-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
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/
|
|
19
|
+
"@evenicanpm/cms": "^2.3.1",
|
|
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,6 +33,8 @@
|
|
|
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",
|
|
35
39
|
"better-auth": "*",
|
|
36
40
|
"graphql": "^16.11.0",
|
|
@@ -50,5 +54,5 @@
|
|
|
50
54
|
"esbuild": "0.19.11"
|
|
51
55
|
}
|
|
52
56
|
},
|
|
53
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "7a5a6a8c3c3dd9efe96fbbb884b21e7681d32550"
|
|
54
58
|
}
|
|
@@ -1,46 +1,71 @@
|
|
|
1
1
|
# Api Manager
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## Selecting a Primary Datasource
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Set the `API_MANAGER_PRIMARY` environment variable to control which datasource is used. Defaults to `"e4"` when unset.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
26
|
+
```ts
|
|
27
|
+
import type { SecondaryDatasource } from "@evenicanpm/storefront-core/src/api-manager/types/Datasource";
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
###
|
|
50
|
+
### `datasources/`
|
|
51
|
+
|
|
52
|
+
Implementations for each supported backend (`e4`, `d365`).
|
|
53
|
+
|
|
54
|
+
### `types/`
|
|
32
55
|
|
|
33
|
-
-
|
|
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
|
-
###
|
|
61
|
+
### `schemas/`
|
|
36
62
|
|
|
37
|
-
|
|
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
|
-
###
|
|
65
|
+
### `lib/`
|
|
41
66
|
|
|
42
|
-
|
|
67
|
+
Utilities and helpers (e.g. category tree builders).
|
|
43
68
|
|
|
44
|
-
###
|
|
69
|
+
### `services/`
|
|
45
70
|
|
|
46
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 {
|
|
15
|
-
import
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
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
|
|
29
|
-
context as
|
|
30
|
-
|
|
31
|
-
|
|
51
|
+
const searchOrdersResults = await searchOrdersAsync(
|
|
52
|
+
context as any,
|
|
53
|
+
searchCriteria,
|
|
54
|
+
null,
|
|
32
55
|
);
|
|
33
|
-
|
|
34
|
-
return
|
|
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
|
-
|
|
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(
|