@tallyui/connector-vendure 3.0.0-next.0 → 3.0.0-next.2

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 CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  ## Errors
4
4
 
5
- Requests with expired or rejected credentials reject with `ConnectorUnauthorizedError` (`code: 'unauthorized'`), defined in `@tallyui/core` and re-exported by this connector. The app should sign in again.
5
+ Requests with expired or rejected credentials reject with `ConnectorUnauthorizedError`, defined in `@tallyui/core` and re-exported by this connector. `error.status` is `401` (`code: 'unauthorized'`, sign in again) or `403` (`code: 'forbidden'`, signed in but not allowed: the pull retries on the store schedule; don't sign out); a signed-out Vendure session is always `401`.
6
6
 
7
7
  Sign-in itself rejects with `SignInError`.
package/dist/index.d.ts CHANGED
@@ -85,7 +85,7 @@ declare const vendureStockReconcile: StockReconcileAdapter;
85
85
  /**
86
86
  * Reads the active channel's currency and tax-inclusivity (ADR-049), and the
87
87
  * default tax zone's enabled, non-customer-group rates keyed by tax category
88
- * id, in integer ppm rounded once. Read-only (ADR-048). Ignores `choice`:
88
+ * id, in integer ppm rounded once, with their rate names for grouping. Read-only (ADR-048). Ignores `choice`:
89
89
  * Vendure's channel is already chosen by the `vendure-token` header sent
90
90
  * with every request, so there is no `pricingContext` either.
91
91
  */
package/dist/index.js CHANGED
@@ -29,9 +29,9 @@ async function signedOut(context) {
29
29
  }
30
30
  async function forbiddenError(context, forbidden) {
31
31
  const out = await signedOut(context);
32
- if (out === true) return new ConnectorUnauthorizedError(`Vendure GraphQL error: ${forbidden[0].message}`);
32
+ if (out === true) return new ConnectorUnauthorizedError(`Vendure GraphQL error: ${forbidden[0].message}`, 401);
33
33
  const what = forbidden.map(({ message, path }) => path?.length ? `${path.join(".")}: ${message}` : message).join("; ");
34
- if (out === false) return new Error(`Vendure GraphQL error: FORBIDDEN: the store refused this request although the session is signed in (a permission is missing): ${what}`);
34
+ if (out === false) return new ConnectorUnauthorizedError(`Vendure GraphQL error: FORBIDDEN: the store refused this request although the session is signed in (a permission is missing): ${what}`, 403);
35
35
  return new Error(`Vendure GraphQL error: FORBIDDEN: ${what} (the session check failed, so this may be transient)`);
36
36
  }
37
37
 
@@ -47,8 +47,8 @@ async function readVendureCapabilities(baseUrl, headers, init = {}) {
47
47
  }
48
48
  if (res.status === 401 || res.status === 403) {
49
49
  const out = await signedOut({ connectorId: "vendure", baseUrl, headers, signal: init.signal });
50
- if (out === true) throw new ConnectorUnauthorizedError2(`Vendure refused ${CAPABILITIES_PATH}: the session is signed out`, res.status);
51
- if (out === false) throw new Error(`Vendure refused ${CAPABILITIES_PATH} although the session is signed in: the CreateOrder permission is missing (HTTP ${res.status})`);
50
+ if (out === true) throw new ConnectorUnauthorizedError2(`Vendure refused ${CAPABILITIES_PATH}: the session is signed out`, 401);
51
+ if (out === false) throw new ConnectorUnauthorizedError2(`Vendure refused ${CAPABILITIES_PATH} although the session is signed in: the CreateOrder permission is missing (HTTP ${res.status})`, 403);
52
52
  return void 0;
53
53
  }
54
54
  if (res.status === 404) return { orderCreate: 1 };
@@ -57,7 +57,7 @@ async function readVendureCapabilities(baseUrl, headers, init = {}) {
57
57
  try {
58
58
  body = await res.json();
59
59
  } catch {
60
- return { orderCreate: 1 };
60
+ return void 0;
61
61
  }
62
62
  return parseInfoCapabilities(body, (reason) => console.warn(`@tallyui/connector-vendure: ${reason}`));
63
63
  }
@@ -824,7 +824,7 @@ var CHANNEL_AND_CATEGORIES_QUERY = `
824
824
  var TAX_RATES_QUERY = `
825
825
  query StoreSettingsTaxRates($zoneId: String!) {
826
826
  taxRates(options: { take: 1000, filter: { zoneId: { eq: $zoneId } } }) {
827
- items { enabled value category { id } customerGroup { id } }
827
+ items { name enabled value category { id } customerGroup { id } }
828
828
  }
829
829
  }
830
830
  `;
@@ -846,11 +846,18 @@ var vendureStoreSettings = async (context, _choice) => {
846
846
  rateItems = ratesBody.data?.taxRates?.items ?? [];
847
847
  }
848
848
  const byCategory = /* @__PURE__ */ new Map();
849
+ const namesByCategory = /* @__PURE__ */ new Map();
849
850
  for (const rate of rateItems) {
850
851
  if (!rate.enabled || rate.customerGroup || !rate.category) continue;
851
852
  byCategory.set(rate.category.id, Math.round(rate.value * 1e4));
853
+ if (typeof rate.name === "string" && rate.name) namesByCategory.set(rate.category.id, rate.name);
854
+ else namesByCategory.delete(rate.category.id);
852
855
  }
853
856
  const defaultCategory = categories.find((c) => c.isDefault) ?? categories[0];
857
+ const taxRateCodes = {};
858
+ const defaultName = defaultCategory && namesByCategory.get(defaultCategory.id);
859
+ if (defaultName) taxRateCodes.default = defaultName;
860
+ for (const [categoryId, name] of namesByCategory) taxRateCodes[categoryId] = name;
854
861
  const taxRatesPpm = {
855
862
  default: defaultCategory ? byCategory.get(defaultCategory.id) ?? 0 : 0
856
863
  };
@@ -859,7 +866,8 @@ var vendureStoreSettings = async (context, _choice) => {
859
866
  return {
860
867
  currency: channel.defaultCurrencyCode,
861
868
  pricesIncludeTax: channel.pricesIncludeTax,
862
- taxRatesPpm
869
+ taxRatesPpm,
870
+ ...Object.keys(taxRateCodes).length > 0 && { taxRateCodes }
863
871
  };
864
872
  };
865
873
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/connector-vendure",
3
- "version": "3.0.0-next.0",
3
+ "version": "3.0.0-next.2",
4
4
  "type": "module",
5
5
  "description": "Vendure connector for Tally UI",
6
6
  "main": "./dist/index.js",
@@ -26,11 +26,11 @@
26
26
  "directory": "connectors/vendure"
27
27
  },
28
28
  "peerDependencies": {
29
- "@tallyui/core": "^3.0.0-next.0"
29
+ "@tallyui/core": "^3.0.0-next.2"
30
30
  },
31
31
  "devDependencies": {
32
- "@tallyui/database": "3.0.0-next.0",
33
- "@tallyui/pos": "3.0.0-next.0",
32
+ "@tallyui/database": "3.0.0-next.2",
33
+ "@tallyui/pos": "3.0.0-next.2",
34
34
  "rxdb": "17.5.0"
35
35
  },
36
36
  "scripts": {
@@ -7,12 +7,12 @@ export const CAPABILITIES_PATH = '/tally/v1/info';
7
7
 
8
8
  /**
9
9
  * Reads the store's contract capabilities (ADR-062) and `taxRounding` (#287) with the
10
- * connector's own headers. A 2xx gives `parseInfoCapabilities`; a 404 or a non-JSON body
11
- * means no plugin and gives `{ orderCreate: 1 }`; a network failure, a 5xx or any other
12
- * non-2xx is unknown and gives `undefined`. The route answers a dead session and a missing
10
+ * connector's own headers. A 404 means an old plugin and gives the default; a network failure,
11
+ * non-2xx other than 404/401/403, non-JSON 2xx or malformed `taxRounding` is unknown and gives
12
+ * `undefined`. The route answers a dead session and a missing
13
13
  * `CreateOrder` permission alike with 403, so a 401/403 asks who is signed in (#279): nobody
14
- * throws `ConnectorUnauthorizedError`, an administrator throws a plain Error naming the
15
- * permission, and a failed check is unknown.
14
+ * throws `ConnectorUnauthorizedError` 401, an administrator throws `ConnectorUnauthorizedError`
15
+ * 403 naming the permission, and a failed check is unknown.
16
16
  */
17
17
  export async function readVendureCapabilities(
18
18
  baseUrl: string,
@@ -28,8 +28,9 @@ export async function readVendureCapabilities(
28
28
  }
29
29
  if (res.status === 401 || res.status === 403) {
30
30
  const out = await signedOut({ connectorId: 'vendure', baseUrl, headers, signal: init.signal });
31
- if (out === true) throw new ConnectorUnauthorizedError(`Vendure refused ${CAPABILITIES_PATH}: the session is signed out`, res.status);
32
- if (out === false) throw new Error(`Vendure refused ${CAPABILITIES_PATH} although the session is signed in: the CreateOrder permission is missing (HTTP ${res.status})`);
31
+ // The probe confirms sign-out; the route's 403 also covers missing permissions.
32
+ if (out === true) throw new ConnectorUnauthorizedError(`Vendure refused ${CAPABILITIES_PATH}: the session is signed out`, 401);
33
+ if (out === false) throw new ConnectorUnauthorizedError(`Vendure refused ${CAPABILITIES_PATH} although the session is signed in: the CreateOrder permission is missing (HTTP ${res.status})`, 403);
33
34
  return undefined;
34
35
  }
35
36
  if (res.status === 404) return { orderCreate: 1 };
@@ -38,7 +39,7 @@ export async function readVendureCapabilities(
38
39
  try {
39
40
  body = await res.json();
40
41
  } catch {
41
- return { orderCreate: 1 };
42
+ return undefined;
42
43
  }
43
44
  return parseInfoCapabilities(body, (reason) => console.warn(`@tallyui/connector-vendure: ${reason}`));
44
45
  }
@@ -81,7 +81,8 @@ export async function gql(
81
81
  if (!res.ok) {
82
82
  const body = await res.json().catch(() => undefined) as GqlBody | undefined;
83
83
  const message = body?.errors?.[0]?.message;
84
- if (res.status === 401 || res.status === 403) throw new ConnectorUnauthorizedError(`Vendure API error: ${res.status}${message ? `: ${message}` : ''}`, res.status);
84
+ // Vendure answers FORBIDDEN with HTTP 200; an HTTP 401/403 comes from a proxy or firewall and is taken at face value.
85
+ if (res.status === 401 || res.status === 403) throw new ConnectorUnauthorizedError(`Vendure API error: ${res.status}${message ? `: ${message}` : ''}`, res.status as 401 | 403);
85
86
  throw new Error(`Vendure API error: ${res.status}${message ? `: ${message}` : ''}`);
86
87
  }
87
88
  const body = await res.json() as GqlBody;
@@ -26,14 +26,15 @@ export async function signedOut(context: SyncContext): Promise<boolean | undefin
26
26
  }
27
27
 
28
28
  /**
29
- * The error for a `FORBIDDEN` answer: only a signed-out session signs the till out. The plain
30
- * errors name everything refused, in the answer's order (each GraphQL `path`, e.g. `product.variants`),
29
+ * The error for a `FORBIDDEN` answer: a signed-out session is 401, a confirmed missing permission
30
+ * is 403, and a failed probe is a plain Error. Errors name everything refused in answer order (each GraphQL `path`, e.g. `product.variants`),
31
31
  * for the store owner's log. One probe answers for the whole response.
32
32
  */
33
33
  export async function forbiddenError(context: SyncContext, forbidden: Array<{ message?: string; path?: ReadonlyArray<string | number> }>): Promise<Error> {
34
34
  const out = await signedOut(context);
35
- if (out === true) return new ConnectorUnauthorizedError(`Vendure GraphQL error: ${forbidden[0].message}`);
35
+ // The probe confirmed a signed-out session even though Vendure answered FORBIDDEN.
36
+ if (out === true) return new ConnectorUnauthorizedError(`Vendure GraphQL error: ${forbidden[0].message}`, 401);
36
37
  const what = forbidden.map(({ message, path }) => (path?.length ? `${path.join('.')}: ${message}` : message)).join('; ');
37
- if (out === false) return new Error(`Vendure GraphQL error: FORBIDDEN: the store refused this request although the session is signed in (a permission is missing): ${what}`);
38
+ if (out === false) return new ConnectorUnauthorizedError(`Vendure GraphQL error: FORBIDDEN: the store refused this request although the session is signed in (a permission is missing): ${what}`, 403);
38
39
  return new Error(`Vendure GraphQL error: FORBIDDEN: ${what} (the session check failed, so this may be transient)`);
39
40
  }
@@ -13,14 +13,14 @@ const CHANNEL_AND_CATEGORIES_QUERY = `
13
13
  const TAX_RATES_QUERY = `
14
14
  query StoreSettingsTaxRates($zoneId: String!) {
15
15
  taxRates(options: { take: 1000, filter: { zoneId: { eq: $zoneId } } }) {
16
- items { enabled value category { id } customerGroup { id } }
16
+ items { name enabled value category { id } customerGroup { id } }
17
17
  }
18
18
  }
19
19
  `;
20
20
 
21
21
  type ActiveChannel = { defaultCurrencyCode: string; pricesIncludeTax: boolean; defaultTaxZone: { id: string } | null };
22
22
  type TaxCategory = { id: string; isDefault: boolean };
23
- type TaxRateItem = { enabled: boolean; value: number; category: { id: string } | null; customerGroup: { id: string } | null };
23
+ type TaxRateItem = { name?: string; enabled: boolean; value: number; category: { id: string } | null; customerGroup: { id: string } | null };
24
24
 
25
25
  /** `gql` throws a plain `Error` for a non-OK response or a GraphQL `errors` body; both become `StoreSettingsError('failed')` here. */
26
26
  async function query(context: SyncContext, source: string, variables?: Record<string, unknown>): Promise<any> {
@@ -34,7 +34,7 @@ async function query(context: SyncContext, source: string, variables?: Record<st
34
34
  /**
35
35
  * Reads the active channel's currency and tax-inclusivity (ADR-049), and the
36
36
  * default tax zone's enabled, non-customer-group rates keyed by tax category
37
- * id, in integer ppm rounded once. Read-only (ADR-048). Ignores `choice`:
37
+ * id, in integer ppm rounded once, with their rate names for grouping. Read-only (ADR-048). Ignores `choice`:
38
38
  * Vendure's channel is already chosen by the `vendure-token` header sent
39
39
  * with every request, so there is no `pricingContext` either.
40
40
  */
@@ -51,10 +51,13 @@ export const vendureStoreSettings = async (context: SyncContext, _choice?: Store
51
51
  }
52
52
 
53
53
  const byCategory = new Map<string, number>();
54
+ const namesByCategory = new Map<string, string>();
54
55
  for (const rate of rateItems) {
55
56
  if (!rate.enabled || rate.customerGroup || !rate.category) continue;
56
57
  // Rounded once, here, at the connector's edge (a backend decimal percentage -> integer ppm).
57
58
  byCategory.set(rate.category.id, Math.round(rate.value * 10_000));
59
+ if (typeof rate.name === 'string' && rate.name) namesByCategory.set(rate.category.id, rate.name);
60
+ else namesByCategory.delete(rate.category.id);
58
61
  }
59
62
 
60
63
  // Vendure's own fallback for a variant created without a category
@@ -65,6 +68,10 @@ export const vendureStoreSettings = async (context: SyncContext, _choice?: Store
65
68
  // at all), `default` is 0 — that is what Vendure itself charges in that
66
69
  // case, never a silent guess.
67
70
  const defaultCategory = categories.find((c) => c.isDefault) ?? categories[0];
71
+ const taxRateCodes: Record<string, string> = {};
72
+ const defaultName = defaultCategory && namesByCategory.get(defaultCategory.id);
73
+ if (defaultName) taxRateCodes.default = defaultName;
74
+ for (const [categoryId, name] of namesByCategory) taxRateCodes[categoryId] = name;
68
75
  const taxRatesPpm: { default: number; [taxClass: string]: number } = {
69
76
  default: defaultCategory ? byCategory.get(defaultCategory.id) ?? 0 : 0,
70
77
  };
@@ -77,5 +84,6 @@ export const vendureStoreSettings = async (context: SyncContext, _choice?: Store
77
84
  currency: channel.defaultCurrencyCode,
78
85
  pricesIncludeTax: channel.pricesIncludeTax,
79
86
  taxRatesPpm,
87
+ ...(Object.keys(taxRateCodes).length > 0 && { taxRateCodes }),
80
88
  };
81
89
  };
@@ -152,7 +152,8 @@ async function gql(
152
152
  signal: context.signal,
153
153
  });
154
154
 
155
- if (res.status === 401 || res.status === 403) throw new ConnectorUnauthorizedError(`Vendure API error: ${res.status}`, res.status);
155
+ // Vendure answers FORBIDDEN with HTTP 200; an HTTP 401/403 comes from a proxy or firewall and is taken at face value.
156
+ if (res.status === 401 || res.status === 403) throw new ConnectorUnauthorizedError(`Vendure API error: ${res.status}`, res.status as 401 | 403);
156
157
  if (!res.ok) throw new Error(`Vendure API error: ${res.status}`);
157
158
  const body: { data?: any; errors?: Array<{ message?: string; path?: Array<string | number>; extensions?: { code?: string } }> } = await res.json();
158
159
  const forbidden = body.errors?.filter((error) => error.extensions?.code === 'FORBIDDEN') ?? [];