@sixtyninecommerce/admin 0.8.0 → 0.9.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.
- package/README.md +2445 -17
- package/dist/api-tokens/index.d.ts +15 -5
- package/dist/api-tokens/index.js +1 -0
- package/dist/apps/event-subscription.d.ts +59 -0
- package/dist/apps/event-subscription.js +32 -0
- package/dist/apps/hook-secret.d.ts +22 -0
- package/dist/apps/hook-secret.js +36 -0
- package/dist/apps/index.d.ts +54 -3
- package/dist/apps/index.js +17 -0
- package/dist/audit/index.d.ts +7 -0
- package/dist/auth/queries.d.ts +5 -1
- package/dist/auth/queries.js +6 -0
- package/dist/bulk/index.d.ts +323 -0
- package/dist/bulk/index.js +162 -0
- package/dist/catalog/assets.d.ts +147 -0
- package/dist/catalog/assets.js +164 -0
- package/dist/catalog/collections.d.ts +109 -0
- package/dist/catalog/collections.js +91 -0
- package/dist/catalog/facets.d.ts +103 -0
- package/dist/catalog/facets.js +100 -0
- package/dist/catalog/index.d.ts +115 -0
- package/dist/catalog/index.js +21 -0
- package/dist/catalog/options.d.ts +89 -0
- package/dist/catalog/options.js +88 -0
- package/dist/catalog/products-bulk.d.ts +21 -0
- package/dist/catalog/products-bulk.js +58 -0
- package/dist/catalog/products.d.ts +489 -0
- package/dist/catalog/products.js +365 -0
- package/dist/catalog/properties.d.ts +234 -0
- package/dist/catalog/properties.js +158 -0
- package/dist/catalog/types.d.ts +174 -0
- package/dist/catalog/types.js +141 -0
- package/dist/catalog/variants.d.ts +274 -0
- package/dist/catalog/variants.js +258 -0
- package/dist/catalog/visibility.d.ts +57 -0
- package/dist/catalog/visibility.js +59 -0
- package/dist/client.d.ts +171 -3
- package/dist/client.js +81 -12
- package/dist/compliance/index.d.ts +88 -0
- package/dist/compliance/index.js +88 -0
- package/dist/customer-data-redaction.d.ts +14 -0
- package/dist/customer-data-redaction.js +22 -0
- package/dist/customers/index.d.ts +428 -17
- package/dist/customers/index.js +322 -15
- package/dist/documents/index.d.ts +513 -16
- package/dist/documents/index.js +229 -3
- package/dist/events/index.d.ts +259 -0
- package/dist/events/index.js +147 -0
- package/dist/feeds/ai.d.ts +46 -0
- package/dist/feeds/ai.js +43 -0
- package/dist/feeds/categories.d.ts +50 -0
- package/dist/feeds/categories.js +56 -0
- package/dist/feeds/configs.d.ts +54 -0
- package/dist/feeds/configs.js +85 -0
- package/dist/feeds/identifier-fields.d.ts +25 -0
- package/dist/feeds/identifier-fields.js +29 -0
- package/dist/feeds/index.d.ts +54 -0
- package/dist/feeds/index.js +46 -0
- package/dist/feeds/preserve.d.ts +38 -0
- package/dist/feeds/preserve.js +74 -0
- package/dist/feeds/rules.d.ts +42 -0
- package/dist/feeds/rules.js +56 -0
- package/dist/feeds/types.d.ts +654 -0
- package/dist/feeds/types.js +135 -0
- package/dist/fulfillments/index.d.ts +360 -0
- package/dist/fulfillments/index.js +425 -0
- package/dist/heads/index.d.ts +60 -0
- package/dist/heads/index.js +45 -0
- package/dist/hooks/assignments.d.ts +90 -0
- package/dist/hooks/assignments.js +53 -0
- package/dist/hooks/index.d.ts +97 -0
- package/dist/hooks/index.js +76 -0
- package/dist/index.d.ts +84 -9
- package/dist/index.js +16 -1
- package/dist/jobs/bulk-action-run.d.ts +65 -0
- package/dist/jobs/bulk-action-run.js +70 -0
- package/dist/locations/index.d.ts +6 -2
- package/dist/meta/index.d.ts +32 -0
- package/dist/orders/commit-edit.d.ts +123 -0
- package/dist/orders/commit-edit.js +142 -0
- package/dist/orders/get.d.ts +722 -0
- package/dist/orders/get.js +530 -0
- package/dist/orders/index.d.ts +105 -20
- package/dist/orders/index.js +44 -0
- package/dist/orders/line-price-preview.d.ts +297 -0
- package/dist/orders/line-price-preview.js +50 -0
- package/dist/orders/manual-discounts.d.ts +132 -0
- package/dist/orders/manual-discounts.js +95 -0
- package/dist/orders/manual-pricing.d.ts +192 -0
- package/dist/orders/manual-pricing.js +72 -0
- package/dist/orders/order-entry.d.ts +566 -0
- package/dist/orders/order-entry.js +129 -0
- package/dist/orders/parcel-charge.d.ts +45 -0
- package/dist/orders/parcel-charge.js +55 -0
- package/dist/orders/plan-edit.d.ts +489 -0
- package/dist/orders/plan-edit.js +93 -0
- package/dist/orders/status.d.ts +235 -0
- package/dist/orders/status.js +96 -0
- package/dist/orders/surcharges.d.ts +75 -2
- package/dist/orders/surcharges.js +110 -4
- package/dist/payments/account-credit.d.ts +171 -0
- package/dist/payments/account-credit.js +107 -0
- package/dist/payments/index.d.ts +234 -9
- package/dist/payments/index.js +90 -0
- package/dist/platform/catalog.d.ts +148 -0
- package/dist/platform/catalog.js +74 -0
- package/dist/platform/entitlements.d.ts +111 -0
- package/dist/platform/entitlements.js +86 -0
- package/dist/platform/legal-entities.d.ts +139 -0
- package/dist/platform/legal-entities.js +126 -0
- package/dist/platform/legal-entity-numbering.d.ts +103 -0
- package/dist/platform/legal-entity-numbering.js +101 -0
- package/dist/platform/organization-membership.d.ts +80 -0
- package/dist/platform/organization-membership.js +71 -0
- package/dist/platform/organization-settings.d.ts +33 -0
- package/dist/platform/organization-settings.js +42 -0
- package/dist/platform/organization-sharing.d.ts +172 -0
- package/dist/platform/organization-sharing.js +120 -0
- package/dist/platform/organizations.d.ts +205 -0
- package/dist/platform/organizations.js +180 -0
- package/dist/platform/provision-store.d.ts +88 -8
- package/dist/platform/provision-store.js +12 -0
- package/dist/platform/reports.d.ts +46 -0
- package/dist/platform/reports.js +31 -0
- package/dist/platform/server-metrics.d.ts +58 -0
- package/dist/platform/server-metrics.js +11 -0
- package/dist/pos/index.d.ts +567 -0
- package/dist/pos/index.js +205 -0
- package/dist/pricing/index.d.ts +405 -0
- package/dist/pricing/index.js +351 -0
- package/dist/provenance/index.d.ts +93 -0
- package/dist/provenance/index.js +53 -0
- package/dist/reports/index.d.ts +199 -0
- package/dist/reports/index.js +154 -0
- package/dist/shipping/parcel-count-suggestion.d.ts +52 -0
- package/dist/shipping/parcel-count-suggestion.js +73 -0
- package/dist/shipping/parcel-rule.d.ts +48 -0
- package/dist/shipping/parcel-rule.js +91 -0
- package/dist/stock/index.d.ts +166 -0
- package/dist/stock/index.js +115 -0
- package/dist/stock/transfers.d.ts +520 -0
- package/dist/stock/transfers.js +437 -0
- package/dist/stored-value/index.d.ts +154 -0
- package/dist/stored-value/index.js +130 -0
- package/dist/tenant/capabilities.d.ts +10 -0
- package/dist/tenant/entitlements.d.ts +35 -0
- package/dist/tenant/entitlements.js +15 -0
- package/dist/tenant/field-groups.d.ts +64 -0
- package/dist/tenant/field-groups.js +34 -0
- package/dist/tenant/legal-entity.d.ts +32 -0
- package/dist/tenant/legal-entity.js +41 -0
- package/dist/tenant/notifications.d.ts +148 -0
- package/dist/tenant/notifications.js +79 -0
- package/dist/tenant/retention.d.ts +10 -4
- package/dist/tenant/settings-sources.d.ts +103 -0
- package/dist/tenant/settings-sources.js +92 -0
- package/dist/types.d.ts +86 -11
- package/dist/uploads/index.d.ts +11 -7
- package/dist/users/index.d.ts +104 -0
- package/dist/users/index.js +80 -0
- package/package.json +3 -2
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { type OrderScope, type Page, type Transport } from '@sixtyninecommerce/core';
|
|
2
|
+
import type { BulkModule, BulkOperation } from '../bulk/index.js';
|
|
3
|
+
import type { JobHandle, JobsModule } from '../jobs/index.js';
|
|
4
|
+
import type { Call, ListOptions } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* A report period (spec §9.3).
|
|
7
|
+
*
|
|
8
|
+
* Each bound is EITHER a date, `YYYY-MM-DD`, read as a whole day in the
|
|
9
|
+
* STORE's time zone (`Channel.customFields.timezone`, else Europe/Amsterdam) —
|
|
10
|
+
* never yours, never the server's — OR an ISO-8601 instant WITH an explicit
|
|
11
|
+
* offset (`2026-09-01T00:00:00+02:00`, `…Z`); a time without an offset is
|
|
12
|
+
* refused (`INVALID_REPORT_PERIOD`, a `ValidationError`).
|
|
13
|
+
*
|
|
14
|
+
* `from` is inclusive. `to` is inclusive as a DATE (`{ from: '2026-09-01',
|
|
15
|
+
* to: '2026-09-30' }` is all of September) and exclusive as an instant. At most
|
|
16
|
+
* 366 days.
|
|
17
|
+
*/
|
|
18
|
+
export interface ReportPeriod {
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ReportOverviewInput {
|
|
23
|
+
period: ReportPeriod;
|
|
24
|
+
/**
|
|
25
|
+
* One stock location of this store (`stockLocationId`). Keeps the orders whose
|
|
26
|
+
* stock was allocated or sold there, or that stated it at entry; an order
|
|
27
|
+
* touching two locations counts at both. A location of another store is
|
|
28
|
+
* refused with `LOCATION_NOT_IN_CHANNEL` (`ValidationError`). Omitted, the
|
|
29
|
+
* client's `tenant.location` (`x-location`) applies, as on every
|
|
30
|
+
* location-taking read. A credential restricted to some locations
|
|
31
|
+
* (`locationIds`) is refused elsewhere with `LOCATION_NOT_ALLOWED`
|
|
32
|
+
* (`PermissionDeniedError`) and, naming none, reads only its own locations'
|
|
33
|
+
* orders.
|
|
34
|
+
*/
|
|
35
|
+
locationId?: string;
|
|
36
|
+
/** One storefront's orders: `CONSUMER` (retail) or `TRADE` (B2B). */
|
|
37
|
+
orderScope?: OrderScope;
|
|
38
|
+
}
|
|
39
|
+
/** The money of one period. Every `…Minor` is an integer in minor units of `currencyCode`. */
|
|
40
|
+
export interface ReportTotals {
|
|
41
|
+
/** Placed, uncancelled orders — every currency. Carts never count. */
|
|
42
|
+
orderCount: number;
|
|
43
|
+
cancelledCount: number;
|
|
44
|
+
/**
|
|
45
|
+
* What those orders BILLED: the sum of each order's total with its goodwill
|
|
46
|
+
* rounding folded in — the same figure its invoice and its payment position
|
|
47
|
+
* read — for the orders in `currencyCode`.
|
|
48
|
+
*/
|
|
49
|
+
revenueMinor: number;
|
|
50
|
+
/** The same orders' billable net totals, the rounding split at each order's own VAT rate. */
|
|
51
|
+
revenueNetMinor: number;
|
|
52
|
+
/** `revenueMinor` ÷ the orders in `currencyCode`, rounded half up. */
|
|
53
|
+
averageOrderValueMinor: number;
|
|
54
|
+
averageOrderValueNetMinor: number;
|
|
55
|
+
/** Orders placed in another currency: in `orderCount`, in no `…Minor` figure. */
|
|
56
|
+
ordersInOtherCurrencies: number;
|
|
57
|
+
}
|
|
58
|
+
/** One day of the store's calendar. */
|
|
59
|
+
export interface ReportDay {
|
|
60
|
+
/** The instant the day begins in the store's zone, ISO-8601 UTC. A DST day is 23 or 25 hours long. */
|
|
61
|
+
startsAt: string;
|
|
62
|
+
orderCount: number;
|
|
63
|
+
revenueMinor: number;
|
|
64
|
+
revenueNetMinor: number;
|
|
65
|
+
}
|
|
66
|
+
export interface ReportOverview {
|
|
67
|
+
/** ISO 4217. The store's default currency; every `…Minor` value is in it. */
|
|
68
|
+
currencyCode: string;
|
|
69
|
+
/** IANA zone the days are cut in — the store's. */
|
|
70
|
+
timeZone: string;
|
|
71
|
+
/** The resolved period, ISO-8601 UTC: `from` inclusive, `to` exclusive. */
|
|
72
|
+
from: string;
|
|
73
|
+
to: string;
|
|
74
|
+
/** The comparison window immediately before, same length. */
|
|
75
|
+
previousFrom: string;
|
|
76
|
+
previousTo: string;
|
|
77
|
+
current: ReportTotals;
|
|
78
|
+
previous: ReportTotals;
|
|
79
|
+
days: ReportDay[];
|
|
80
|
+
newCustomers: number;
|
|
81
|
+
previousNewCustomers: number;
|
|
82
|
+
/** Live, not period-scoped: placed orders whose goods have not all been handed off. */
|
|
83
|
+
awaitingFulfillment: number;
|
|
84
|
+
/** Placed orders of the period by lifecycle (`open`, `completed`, `cancelled`). */
|
|
85
|
+
statusBreakdown: Array<{
|
|
86
|
+
status: string;
|
|
87
|
+
count: number;
|
|
88
|
+
}>;
|
|
89
|
+
topProducts: Array<{
|
|
90
|
+
productVariantId: string;
|
|
91
|
+
productId: string;
|
|
92
|
+
name: string;
|
|
93
|
+
sku: string;
|
|
94
|
+
unitsSold: number;
|
|
95
|
+
}>;
|
|
96
|
+
}
|
|
97
|
+
export interface PopularVariantsInput {
|
|
98
|
+
/** 1–50, default 8. */
|
|
99
|
+
take?: number;
|
|
100
|
+
/** 1–3650, default 90. */
|
|
101
|
+
sinceDays?: number;
|
|
102
|
+
}
|
|
103
|
+
export interface PopularVariant {
|
|
104
|
+
id: string;
|
|
105
|
+
productId: string;
|
|
106
|
+
name: string;
|
|
107
|
+
sku: string;
|
|
108
|
+
}
|
|
109
|
+
export interface LowStockInput extends ListOptions {
|
|
110
|
+
/** Rows whose `available` is at or below this; a whole number, 0–1,000,000. */
|
|
111
|
+
threshold: number;
|
|
112
|
+
/**
|
|
113
|
+
* One stock location of this store. Omitted: the client's `tenant.location`
|
|
114
|
+
* (`x-location`), else every location this credential may act at.
|
|
115
|
+
*/
|
|
116
|
+
locationId?: string;
|
|
117
|
+
}
|
|
118
|
+
/** One variant at one stock location. */
|
|
119
|
+
export interface LowStockVariant {
|
|
120
|
+
productVariantId: string;
|
|
121
|
+
productId: string;
|
|
122
|
+
sku: string;
|
|
123
|
+
name: string;
|
|
124
|
+
stockLocationId: string;
|
|
125
|
+
stockLocationName: string;
|
|
126
|
+
stockOnHand: number;
|
|
127
|
+
stockAllocated: number;
|
|
128
|
+
/** Units the out-of-stock threshold holds back from sale: `max(0, threshold)`. */
|
|
129
|
+
reserved: number;
|
|
130
|
+
/**
|
|
131
|
+
* `stockOnHand − stockAllocated − threshold` — the same figure `admin.stock`
|
|
132
|
+
* reports; negative when more is allocated than can be sold.
|
|
133
|
+
*/
|
|
134
|
+
available: number;
|
|
135
|
+
}
|
|
136
|
+
/** The reports `export` can produce. */
|
|
137
|
+
export type ReportExportKind = 'overview';
|
|
138
|
+
export interface ReportExportFilter {
|
|
139
|
+
period: ReportPeriod;
|
|
140
|
+
/**
|
|
141
|
+
* Omitted, the client's (or the call's) `tenant.location` is used — the same
|
|
142
|
+
* fallback `overview` gets from `x-location` — so a file and the screen it
|
|
143
|
+
* mirrors cover the same place.
|
|
144
|
+
*/
|
|
145
|
+
locationId?: string;
|
|
146
|
+
orderScope?: OrderScope;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A finished export: a bulk operation whose `resultUrl` is a short-lived
|
|
150
|
+
* signed link to the JSONL — one line per store day:
|
|
151
|
+
* `{ date, startsAt, timeZone, currencyCode, orderCount, revenueMinor, revenueNetMinor }`.
|
|
152
|
+
* Read `status` before the file: a failed run carries `error.code`.
|
|
153
|
+
*/
|
|
154
|
+
export type ReportExport = Omit<BulkOperation, 'entity'> & {
|
|
155
|
+
/** Export-only report entities are not among the importable `BulkEntity` values. */
|
|
156
|
+
entity: 'report_overview';
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Tenant reporting (spec §9.2 `reports`): the order-analytics surface the
|
|
160
|
+
* dashboard reads, with money in integer minor units + `currencyCode` and time
|
|
161
|
+
* as ISO-8601 UTC (§9.3).
|
|
162
|
+
*
|
|
163
|
+
* Everything is scoped to the client's store; nothing here reads another.
|
|
164
|
+
* `overview` needs `ReadOrder`, `lowStock` `ReadCatalog` or `ReadProduct`,
|
|
165
|
+
* `popularVariants` `ReadOrder`, `export` `ReadOrder`. The cross-store fleet is
|
|
166
|
+
* `admin.platform.reports.fleet` (SuperAdmin session only).
|
|
167
|
+
*/
|
|
168
|
+
export interface ReportsModule {
|
|
169
|
+
/** Orders, revenue and customers of a period, by the store's own days. */
|
|
170
|
+
overview(input: ReportOverviewInput, options?: Call): Promise<ReportOverview>;
|
|
171
|
+
/**
|
|
172
|
+
* Variants ranked by units sold in placed orders of the last `sinceDays`
|
|
173
|
+
* days, most first. A `Page` because every list is one; it is always a
|
|
174
|
+
* single, final page.
|
|
175
|
+
*/
|
|
176
|
+
popularVariants(input?: PopularVariantsInput, options?: Call): Promise<Page<PopularVariant>>;
|
|
177
|
+
/**
|
|
178
|
+
* Variants at or below `threshold` available units, per stock location,
|
|
179
|
+
* lowest first. Untracked variants are never low.
|
|
180
|
+
*/
|
|
181
|
+
lowStock(input: LowStockInput): Promise<Page<LowStockVariant>>;
|
|
182
|
+
/**
|
|
183
|
+
* Write a report to a file, as a job (`kind: report.export`). `wait()`
|
|
184
|
+
* resolves with the operation and a freshly signed `resultUrl`. The filter's
|
|
185
|
+
* shape is checked when you call; its meaning (a real period, a location of
|
|
186
|
+
* this store) when the job runs, so a bad location fails the JOB. A
|
|
187
|
+
* location-restricted credential must name one of its own locations
|
|
188
|
+
* (`LOCATION_NOT_ALLOWED` otherwise) — a store-wide file is what its
|
|
189
|
+
* restriction withholds.
|
|
190
|
+
*/
|
|
191
|
+
export(kind: ReportExportKind, filter: ReportExportFilter, options?: Call): Promise<JobHandle<ReportExport>>;
|
|
192
|
+
}
|
|
193
|
+
/** Only the §9.3 generation is selected — never the deprecated Float/String fields. */
|
|
194
|
+
export declare const REPORT_OVERVIEW_QUERY = "\n query ReportOverview($range: ReportPeriodInput!, $locationId: ID, $orderScope: OrderScope) {\n commerceOverviewMetrics(range: $range, locationId: $locationId, orderScope: $orderScope) {\n currencyCode\n timeZone\n from\n to\n previousFrom\n previousTo\n current { \n orderCount\n cancelledCount\n revenueMinor\n revenueNetMinor\n averageOrderValueMinor\n averageOrderValueNetMinor\n ordersInOtherCurrencies\n }\n previous { \n orderCount\n cancelledCount\n revenueMinor\n revenueNetMinor\n averageOrderValueMinor\n averageOrderValueNetMinor\n ordersInOtherCurrencies\n }\n revenueSeries { startsAt valueMinor valueNetMinor }\n orderSeries { startsAt value }\n newCustomers\n previousNewCustomers\n awaitingFulfillment\n statusBreakdown { state count }\n topProducts { productVariantId productId name sku unitsSold }\n }\n }\n";
|
|
195
|
+
export declare const POPULAR_VARIANTS_QUERY = "\n query ReportPopularVariants($take: Int, $sinceDays: Int) {\n popularProductVariantsInChannel(take: $take, sinceDays: $sinceDays) {\n id\n productId\n name\n sku\n }\n }\n";
|
|
196
|
+
export declare const LOW_STOCK_QUERY = "\n query ReportLowStock($threshold: Int!, $locationId: ID, $options: LowStockVariantListOptions) {\n lowStockVariants(threshold: $threshold, locationId: $locationId, options: $options) {\n items {\n productVariantId\n productId\n sku\n name\n stockLocationId\n stockLocationName\n stockOnHand\n stockAllocated\n reserved\n available\n }\n totalItems\n }\n }\n";
|
|
197
|
+
export declare function createReportsModule(transport: Transport, bulk: BulkModule, jobs: JobsModule,
|
|
198
|
+
/** The client's `tenant.location`, read per call (a derived client has its own). */
|
|
199
|
+
clientLocation?: () => string | undefined): ReportsModule;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createPage, pageFromVendureList, readOffsetCursor, } from '@sixtyninecommerce/core';
|
|
2
|
+
const TOTALS_FIELDS = /* GraphQL */ `
|
|
3
|
+
orderCount
|
|
4
|
+
cancelledCount
|
|
5
|
+
revenueMinor
|
|
6
|
+
revenueNetMinor
|
|
7
|
+
averageOrderValueMinor
|
|
8
|
+
averageOrderValueNetMinor
|
|
9
|
+
ordersInOtherCurrencies
|
|
10
|
+
`;
|
|
11
|
+
/** Only the §9.3 generation is selected — never the deprecated Float/String fields. */
|
|
12
|
+
export const REPORT_OVERVIEW_QUERY = /* GraphQL */ `
|
|
13
|
+
query ReportOverview($range: ReportPeriodInput!, $locationId: ID, $orderScope: OrderScope) {
|
|
14
|
+
commerceOverviewMetrics(range: $range, locationId: $locationId, orderScope: $orderScope) {
|
|
15
|
+
currencyCode
|
|
16
|
+
timeZone
|
|
17
|
+
from
|
|
18
|
+
to
|
|
19
|
+
previousFrom
|
|
20
|
+
previousTo
|
|
21
|
+
current { ${TOTALS_FIELDS} }
|
|
22
|
+
previous { ${TOTALS_FIELDS} }
|
|
23
|
+
revenueSeries { startsAt valueMinor valueNetMinor }
|
|
24
|
+
orderSeries { startsAt value }
|
|
25
|
+
newCustomers
|
|
26
|
+
previousNewCustomers
|
|
27
|
+
awaitingFulfillment
|
|
28
|
+
statusBreakdown { state count }
|
|
29
|
+
topProducts { productVariantId productId name sku unitsSold }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
export const POPULAR_VARIANTS_QUERY = /* GraphQL */ `
|
|
34
|
+
query ReportPopularVariants($take: Int, $sinceDays: Int) {
|
|
35
|
+
popularProductVariantsInChannel(take: $take, sinceDays: $sinceDays) {
|
|
36
|
+
id
|
|
37
|
+
productId
|
|
38
|
+
name
|
|
39
|
+
sku
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
export const LOW_STOCK_QUERY = /* GraphQL */ `
|
|
44
|
+
query ReportLowStock($threshold: Int!, $locationId: ID, $options: LowStockVariantListOptions) {
|
|
45
|
+
lowStockVariants(threshold: $threshold, locationId: $locationId, options: $options) {
|
|
46
|
+
items {
|
|
47
|
+
productVariantId
|
|
48
|
+
productId
|
|
49
|
+
sku
|
|
50
|
+
name
|
|
51
|
+
stockLocationId
|
|
52
|
+
stockLocationName
|
|
53
|
+
stockOnHand
|
|
54
|
+
stockAllocated
|
|
55
|
+
reserved
|
|
56
|
+
available
|
|
57
|
+
}
|
|
58
|
+
totalItems
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
const DEFAULT_LOW_STOCK_PAGE = 50;
|
|
63
|
+
/** The bulk entity a report kind is exported as. */
|
|
64
|
+
const EXPORT_ENTITY = { overview: 'report_overview' };
|
|
65
|
+
/** Filter keys travel as the service spells them — see `bulk/index.ts`'s `FILTER_PASSTHROUGH`. */
|
|
66
|
+
const FILTER_PASSTHROUGH = new Set(['filter']);
|
|
67
|
+
export function createReportsModule(transport, bulk, jobs,
|
|
68
|
+
/** The client's `tenant.location`, read per call (a derived client has its own). */
|
|
69
|
+
clientLocation = () => undefined) {
|
|
70
|
+
const lowStockPage = async (input, skip, take) => {
|
|
71
|
+
const data = await transport.graphql({
|
|
72
|
+
api: 'admin',
|
|
73
|
+
query: LOW_STOCK_QUERY,
|
|
74
|
+
variables: {
|
|
75
|
+
threshold: input.threshold,
|
|
76
|
+
locationId: input.locationId ?? null,
|
|
77
|
+
options: { skip, take },
|
|
78
|
+
},
|
|
79
|
+
}, input);
|
|
80
|
+
return pageFromVendureList(data.lowStockVariants, skip, take, (cursor) => lowStockPage(input, readOffsetCursor(cursor), take));
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
async overview(input, options) {
|
|
84
|
+
const data = await transport.graphql({
|
|
85
|
+
api: 'admin',
|
|
86
|
+
query: REPORT_OVERVIEW_QUERY,
|
|
87
|
+
variables: {
|
|
88
|
+
range: { from: input.period.from, to: input.period.to },
|
|
89
|
+
locationId: input.locationId ?? null,
|
|
90
|
+
orderScope: input.orderScope ?? null,
|
|
91
|
+
},
|
|
92
|
+
}, options);
|
|
93
|
+
const { revenueSeries, orderSeries, statusBreakdown, ...rest } = data.commerceOverviewMetrics;
|
|
94
|
+
// The two series are the same buckets; joined on the instant rather than
|
|
95
|
+
// the index so a reordering on the server could never pair two days.
|
|
96
|
+
const orders = new Map(orderSeries.map((point) => [point.startsAt, point.value]));
|
|
97
|
+
return {
|
|
98
|
+
...rest,
|
|
99
|
+
days: revenueSeries.map((point) => ({
|
|
100
|
+
startsAt: point.startsAt,
|
|
101
|
+
orderCount: orders.get(point.startsAt) ?? 0,
|
|
102
|
+
revenueMinor: point.valueMinor ?? 0,
|
|
103
|
+
revenueNetMinor: point.valueNetMinor ?? 0,
|
|
104
|
+
})),
|
|
105
|
+
statusBreakdown: statusBreakdown.map((row) => ({ status: row.state, count: row.count })),
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
async popularVariants(input, options) {
|
|
109
|
+
const data = await transport.graphql({
|
|
110
|
+
api: 'admin',
|
|
111
|
+
query: POPULAR_VARIANTS_QUERY,
|
|
112
|
+
variables: { take: input?.take ?? null, sinceDays: input?.sinceDays ?? null },
|
|
113
|
+
}, options);
|
|
114
|
+
const items = data.popularProductVariantsInChannel;
|
|
115
|
+
return createPage(items, { nextCursor: null, hasNext: false, total: items.length }, () => {
|
|
116
|
+
// Unreachable while `hasNext` is false — `Page.next()` short-circuits.
|
|
117
|
+
throw new Error('popularVariants returns one ranked list; there is no next page');
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
lowStock(input) {
|
|
121
|
+
const take = input.limit ?? DEFAULT_LOW_STOCK_PAGE;
|
|
122
|
+
const skip = input.after === undefined ? 0 : readOffsetCursor(input.after);
|
|
123
|
+
return lowStockPage(input, skip, take);
|
|
124
|
+
},
|
|
125
|
+
async export(kind, filter, options) {
|
|
126
|
+
// The worker runs without the caller's `x-location`, so the fallback
|
|
127
|
+
// `overview` gets from that header is applied here, explicitly.
|
|
128
|
+
const locationId = filter.locationId ?? options?.tenant?.location ?? clientLocation();
|
|
129
|
+
const { jobId } = await transport.rest('plugins', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
path: '/bulk/exports',
|
|
132
|
+
body: {
|
|
133
|
+
entity: EXPORT_ENTITY[kind],
|
|
134
|
+
filter: {
|
|
135
|
+
from: filter.period.from,
|
|
136
|
+
to: filter.period.to,
|
|
137
|
+
...(locationId === undefined ? {} : { location_id: locationId }),
|
|
138
|
+
...(filter.orderScope === undefined ? {} : { order_scope: filter.orderScope }),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
}, { ...options, preserveKeys: FILTER_PASSTHROUGH });
|
|
142
|
+
return {
|
|
143
|
+
runId: jobId,
|
|
144
|
+
async wait(waitOptions) {
|
|
145
|
+
// Wait on the JOB, then read the operation once: its signed link is
|
|
146
|
+
// minted per read, so one minted while the run was going would have
|
|
147
|
+
// expired by the time the file existed.
|
|
148
|
+
await jobs.wait(jobId, { ...options, ...waitOptions });
|
|
149
|
+
return (await bulk.get(jobId, options));
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Transport } from '@sixtyninecommerce/core';
|
|
2
|
+
import type { Call } from '../types.js';
|
|
3
|
+
export declare const PARCEL_SUGGESTION_MAX_ITEMS = 500;
|
|
4
|
+
export declare const PARCEL_SUGGESTION_MAX_QUANTITY = 100000;
|
|
5
|
+
export interface ParcelCountSuggestionItem {
|
|
6
|
+
productVariantId: string;
|
|
7
|
+
/** Whole units, 1..100,000. Items of one variant are summed by the engine. */
|
|
8
|
+
quantity: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ParcelCountSuggestionInput {
|
|
11
|
+
/** 1..500 items, from any number of the active channel's orders. */
|
|
12
|
+
items: ParcelCountSuggestionItem[];
|
|
13
|
+
/** ISO 3166-1 alpha-2. Only sets `destinationRequiresCustoms`; the count ignores it. */
|
|
14
|
+
destinationCountryCode?: string | null;
|
|
15
|
+
}
|
|
16
|
+
/** The active channel's parcel-count rule applied to the items. */
|
|
17
|
+
export interface ParcelCountSuggestion {
|
|
18
|
+
/** 1..50. */
|
|
19
|
+
count: number;
|
|
20
|
+
/** The unclamped count; 0 when nothing was counted. */
|
|
21
|
+
raw: number;
|
|
22
|
+
/** False only when no rule applied (then `count` is 1 and `reason` is `NO_PARCEL_RULE`). */
|
|
23
|
+
resolvable: boolean;
|
|
24
|
+
/** Every counted item had all the data the rule asks for. */
|
|
25
|
+
exact: boolean;
|
|
26
|
+
/** `raw` exceeded 50. */
|
|
27
|
+
capped: boolean;
|
|
28
|
+
/** The channel's items per parcel, when set. */
|
|
29
|
+
capacity: number | null;
|
|
30
|
+
/** The channel's maximum parcel weight in grams, when set. */
|
|
31
|
+
maxWeightGrams: number | null;
|
|
32
|
+
/** Variants counted without a capacity: `count` is a lower bound. */
|
|
33
|
+
unratedLines: number;
|
|
34
|
+
/** Variants counted without a package weight under a maximum: `count` is a lower bound. */
|
|
35
|
+
unweighedLines: number;
|
|
36
|
+
/** `NO_PARCEL_RULE`, or null. A code: the caller owns the wording, and may meet new codes. */
|
|
37
|
+
reason: string | null;
|
|
38
|
+
/** `UNIT_OVER_MAX_WEIGHT` and/or `CAPPED`; may gain codes. */
|
|
39
|
+
warnings: string[];
|
|
40
|
+
/** Whether a parcel there needs a customs declaration; null without a destination. */
|
|
41
|
+
destinationRequiresCustoms: boolean | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* `admin.shipping.parcelCountSuggestion`: how many parcels the active
|
|
45
|
+
* channel's rule gives a set of variant + quantity items. Needs `ReadCatalog`
|
|
46
|
+
* or `ReadFulfillment`. A malformed input is refused before any request with
|
|
47
|
+
* `PARCEL_COUNT_INPUT_INVALID` ({@link ValidationError}); an unknown variant,
|
|
48
|
+
* or another store's, is `PARCEL_COUNT_VARIANT_NOT_FOUND` (`NotFoundError`).
|
|
49
|
+
*/
|
|
50
|
+
export type ParcelCountSuggestionMethod = (input: ParcelCountSuggestionInput, options?: Call) => Promise<ParcelCountSuggestion>;
|
|
51
|
+
export declare const PARCEL_COUNT_SUGGESTION_QUERY = "\n query ParcelCountSuggestion($input: ParcelCountSuggestionInput!) {\n parcelCountSuggestion(input: $input) {\n count\n raw\n resolvable\n exact\n capped\n capacity\n maxWeightGrams\n unratedLines\n unweighedLines\n reason\n warnings\n destinationRequiresCustoms\n }\n }\n";
|
|
52
|
+
export declare function createParcelCountSuggestion(transport: Transport): ParcelCountSuggestionMethod;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ValidationError } from '@sixtyninecommerce/core';
|
|
2
|
+
// Mirror the engine's bounds (`services/vendure/src/parcel-count/`), which are
|
|
3
|
+
// the authority; these only refuse a bad input before it leaves the process.
|
|
4
|
+
export const PARCEL_SUGGESTION_MAX_ITEMS = 500;
|
|
5
|
+
export const PARCEL_SUGGESTION_MAX_QUANTITY = 100_000;
|
|
6
|
+
export const PARCEL_COUNT_SUGGESTION_QUERY = /* GraphQL */ `
|
|
7
|
+
query ParcelCountSuggestion($input: ParcelCountSuggestionInput!) {
|
|
8
|
+
parcelCountSuggestion(input: $input) {
|
|
9
|
+
count
|
|
10
|
+
raw
|
|
11
|
+
resolvable
|
|
12
|
+
exact
|
|
13
|
+
capped
|
|
14
|
+
capacity
|
|
15
|
+
maxWeightGrams
|
|
16
|
+
unratedLines
|
|
17
|
+
unweighedLines
|
|
18
|
+
reason
|
|
19
|
+
warnings
|
|
20
|
+
destinationRequiresCustoms
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
function invalid(field, message) {
|
|
25
|
+
return new ValidationError({
|
|
26
|
+
code: 'PARCEL_COUNT_INPUT_INVALID',
|
|
27
|
+
message,
|
|
28
|
+
status: 400,
|
|
29
|
+
service: 'vendure',
|
|
30
|
+
details: [{ field, code: 'PARCEL_COUNT_INPUT_INVALID', message }],
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function assertInput(input) {
|
|
34
|
+
const items = input?.items;
|
|
35
|
+
if (!Array.isArray(items) || items.length < 1 || items.length > PARCEL_SUGGESTION_MAX_ITEMS) {
|
|
36
|
+
throw invalid('items', `Pass between 1 and ${PARCEL_SUGGESTION_MAX_ITEMS} items.`);
|
|
37
|
+
}
|
|
38
|
+
items.forEach((item, index) => {
|
|
39
|
+
if (typeof item?.productVariantId !== 'string' || item.productVariantId === '') {
|
|
40
|
+
throw invalid(`items[${index}].productVariantId`, 'productVariantId is required.');
|
|
41
|
+
}
|
|
42
|
+
const quantity = item.quantity;
|
|
43
|
+
if (typeof quantity !== 'number' ||
|
|
44
|
+
!Number.isInteger(quantity) ||
|
|
45
|
+
quantity < 1 ||
|
|
46
|
+
quantity > PARCEL_SUGGESTION_MAX_QUANTITY) {
|
|
47
|
+
throw invalid(`items[${index}].quantity`, `quantity must be a whole number from 1 to ${PARCEL_SUGGESTION_MAX_QUANTITY.toLocaleString('en')}; got ${JSON.stringify(quantity)}.`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const country = input.destinationCountryCode;
|
|
51
|
+
if (country !== undefined && country !== null && !/^[A-Za-z]{2}$/.test(country.trim())) {
|
|
52
|
+
throw invalid('destinationCountryCode', 'destinationCountryCode must be an ISO 3166-1 alpha-2 code, such as NL.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function createParcelCountSuggestion(transport) {
|
|
56
|
+
return async (input, options) => {
|
|
57
|
+
assertInput(input);
|
|
58
|
+
const data = await transport.graphql({
|
|
59
|
+
api: 'admin',
|
|
60
|
+
query: PARCEL_COUNT_SUGGESTION_QUERY,
|
|
61
|
+
variables: {
|
|
62
|
+
input: {
|
|
63
|
+
items: input.items.map(({ productVariantId, quantity }) => ({
|
|
64
|
+
productVariantId,
|
|
65
|
+
quantity,
|
|
66
|
+
})),
|
|
67
|
+
destinationCountryCode: input.destinationCountryCode ?? null,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}, options);
|
|
71
|
+
return data.parcelCountSuggestion;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Transport } from '@sixtyninecommerce/core';
|
|
2
|
+
import type { Call } from '../types.js';
|
|
3
|
+
export declare const PARCEL_CAPACITY_MAX = 10000;
|
|
4
|
+
export declare const PARCEL_WEIGHT_GRAMS_MAX = 70000;
|
|
5
|
+
/** How the active channel counts parcels. `null` means "not set". */
|
|
6
|
+
export interface ParcelRule {
|
|
7
|
+
channelId: string;
|
|
8
|
+
/**
|
|
9
|
+
* Units that fill one parcel unless a variant sets its own. `null` is no
|
|
10
|
+
* rule: every order counts as one parcel.
|
|
11
|
+
*/
|
|
12
|
+
defaultParcelCapacity: number | null;
|
|
13
|
+
/** Extra lower bound from known package weights, in grams. `null` is no limit. */
|
|
14
|
+
maxParcelWeightGrams: number | null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A PATCH: a key left out is unchanged, `null` clears it. At least one key is
|
|
18
|
+
* required.
|
|
19
|
+
*/
|
|
20
|
+
export interface SetParcelRuleInput {
|
|
21
|
+
/** Integer 1..10,000, or `null` to remove the rule. */
|
|
22
|
+
defaultParcelCapacity?: number | null;
|
|
23
|
+
/** Integer grams 1..70,000, or `null` to remove the limit. */
|
|
24
|
+
maxParcelWeightGrams?: number | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The ACTIVE channel's parcel-count rule (`admin.shipping.parcelRule`),
|
|
28
|
+
* stored as Channel custom fields and written through core `updateChannel`.
|
|
29
|
+
*
|
|
30
|
+
* Reading needs an authenticated administrator on the channel; writing needs
|
|
31
|
+
* `UpdateChannel`. A value out of range is refused here with
|
|
32
|
+
* `PARCEL_RULE_INVALID` ({@link ValidationError}) before any request, and by
|
|
33
|
+
* the engine's own custom-field validation for any other writer.
|
|
34
|
+
*
|
|
35
|
+
* The engine's storefront pricing reads the rule from its channel cache, so a
|
|
36
|
+
* change reaches another process after that cache refreshes.
|
|
37
|
+
*/
|
|
38
|
+
export interface ParcelRuleModule {
|
|
39
|
+
get(options?: Call): Promise<ParcelRule>;
|
|
40
|
+
set(input: SetParcelRuleInput, options?: Call): Promise<ParcelRule>;
|
|
41
|
+
}
|
|
42
|
+
/** `admin.shipping` (spec §9.2 `shipping`). */
|
|
43
|
+
export interface ShippingModule {
|
|
44
|
+
parcelRule: ParcelRuleModule;
|
|
45
|
+
}
|
|
46
|
+
export declare const PARCEL_RULE_QUERY = "\n query ParcelRule {\n activeChannel { \n id\n customFields {\n defaultParcelCapacity\n maxParcelWeightGrams\n }\n }\n }\n";
|
|
47
|
+
export declare const SET_PARCEL_RULE_MUTATION = "\n mutation SetParcelRule($input: UpdateChannelInput!) {\n updateChannel(input: $input) {\n ... on Channel { \n id\n customFields {\n defaultParcelCapacity\n maxParcelWeightGrams\n }\n }\n ... on ErrorResult { errorCode message }\n }\n }\n";
|
|
48
|
+
export declare function createShippingModule(transport: Transport): ShippingModule;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { errorFromErrorResult, isErrorResult, ValidationError, } from '@sixtyninecommerce/core';
|
|
2
|
+
// Mirrors `MAX_PARCEL_CAPACITY` / `MAX_PARCEL_WEIGHT_GRAMS` in
|
|
3
|
+
// `vendure-plugin-shipping-calculator/src/parcels/parcel-rule-fields.ts`. The
|
|
4
|
+
// engine's custom-field `min`/`max` is the authority; these only refuse a bad
|
|
5
|
+
// value before it leaves the process. Change both together.
|
|
6
|
+
export const PARCEL_CAPACITY_MAX = 10_000;
|
|
7
|
+
export const PARCEL_WEIGHT_GRAMS_MAX = 70_000;
|
|
8
|
+
const RULE_FIELDS = /* GraphQL */ `
|
|
9
|
+
id
|
|
10
|
+
customFields {
|
|
11
|
+
defaultParcelCapacity
|
|
12
|
+
maxParcelWeightGrams
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
export const PARCEL_RULE_QUERY = /* GraphQL */ `
|
|
16
|
+
query ParcelRule {
|
|
17
|
+
activeChannel { ${RULE_FIELDS} }
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
export const SET_PARCEL_RULE_MUTATION = /* GraphQL */ `
|
|
21
|
+
mutation SetParcelRule($input: UpdateChannelInput!) {
|
|
22
|
+
updateChannel(input: $input) {
|
|
23
|
+
... on Channel { ${RULE_FIELDS} }
|
|
24
|
+
... on ErrorResult { errorCode message }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
function toRule(channel) {
|
|
29
|
+
return {
|
|
30
|
+
channelId: channel.id,
|
|
31
|
+
defaultParcelCapacity: channel.customFields?.defaultParcelCapacity ?? null,
|
|
32
|
+
maxParcelWeightGrams: channel.customFields?.maxParcelWeightGrams ?? null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function invalid(field, message) {
|
|
36
|
+
return new ValidationError({
|
|
37
|
+
code: 'PARCEL_RULE_INVALID',
|
|
38
|
+
message,
|
|
39
|
+
status: 400,
|
|
40
|
+
service: 'vendure',
|
|
41
|
+
details: [{ field, code: 'PARCEL_RULE_INVALID', message }],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function assertBound(field, value, max, unit) {
|
|
45
|
+
if (value === undefined || value === null)
|
|
46
|
+
return;
|
|
47
|
+
if (typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= max)
|
|
48
|
+
return;
|
|
49
|
+
throw invalid(field, `${field} must be a whole number of ${unit} from 1 to ${max.toLocaleString('en')}, or null to clear it; got ${JSON.stringify(value)}.`);
|
|
50
|
+
}
|
|
51
|
+
function customFieldsOf(input) {
|
|
52
|
+
const fields = {};
|
|
53
|
+
if (input.defaultParcelCapacity !== undefined) {
|
|
54
|
+
fields.defaultParcelCapacity = input.defaultParcelCapacity;
|
|
55
|
+
}
|
|
56
|
+
if (input.maxParcelWeightGrams !== undefined) {
|
|
57
|
+
fields.maxParcelWeightGrams = input.maxParcelWeightGrams;
|
|
58
|
+
}
|
|
59
|
+
return fields;
|
|
60
|
+
}
|
|
61
|
+
export function createShippingModule(transport) {
|
|
62
|
+
const get = async (options) => {
|
|
63
|
+
const data = await transport.graphql({ api: 'admin', query: PARCEL_RULE_QUERY }, options);
|
|
64
|
+
return toRule(data.activeChannel);
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
parcelRule: {
|
|
68
|
+
get,
|
|
69
|
+
async set(input, options) {
|
|
70
|
+
assertBound('defaultParcelCapacity', input?.defaultParcelCapacity, PARCEL_CAPACITY_MAX, 'items');
|
|
71
|
+
assertBound('maxParcelWeightGrams', input?.maxParcelWeightGrams, PARCEL_WEIGHT_GRAMS_MAX, 'grams');
|
|
72
|
+
const customFields = customFieldsOf(input ?? {});
|
|
73
|
+
if (Object.keys(customFields).length === 0) {
|
|
74
|
+
throw invalid('input', 'Pass defaultParcelCapacity and/or maxParcelWeightGrams; an empty input changes nothing.');
|
|
75
|
+
}
|
|
76
|
+
// `updateChannel` needs the id, and the active channel is the one the
|
|
77
|
+
// client is scoped to — read it with the SAME call options, so a
|
|
78
|
+
// per-call tenant writes its own channel rather than the client's.
|
|
79
|
+
const { channelId } = await get(options);
|
|
80
|
+
const data = await transport.graphql({
|
|
81
|
+
api: 'admin',
|
|
82
|
+
query: SET_PARCEL_RULE_MUTATION,
|
|
83
|
+
variables: { input: { id: channelId, customFields } },
|
|
84
|
+
}, options);
|
|
85
|
+
if (isErrorResult(data.updateChannel))
|
|
86
|
+
throw errorFromErrorResult(data.updateChannel);
|
|
87
|
+
return toRule(data.updateChannel);
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|