@tallyui/connector-vendure 3.0.2 → 3.0.4

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
@@ -5,3 +5,13 @@
5
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`.
8
+
9
+ ## Customers
10
+
11
+ `searchCustomers`, `createCustomer` and `getCustomer` are online only and run in the session's channel, selected by the `vendure-token` header.
12
+
13
+ The till's administrator needs `ReadCustomer` to search and look up customers, and `CreateCustomer` to create them. Without these permissions, the calls reject with `ConnectorUnauthorizedError` status `403`.
14
+
15
+ A duplicate email rejects with `CustomerServiceError` code `invalid`. The vendurepos walk-in customer is left out of search results.
16
+
17
+ Vendure's `CreateCustomerInput` requires `firstName` and `lastName`; leaving them out fails GraphQL input validation (recorded on Vendure 3.7.3). So `createCustomer` with only an email sends both as `''`, which Vendure 3.7.3 accepts: it creates the customer with empty names, and the till shows its email as the name.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ConnectorAuth, ProductTraits, CollectionSync, ReplicationAdapter, StockReconcileAdapter, SyncContext, StoreSettingsChoice, StoreSettings, TallyConnector } from '@tallyui/core';
1
+ import { ConnectorAuth, ProductTraits, CollectionSync, ReplicationAdapter, StockReconcileAdapter, SyncContext, StoreSettingsChoice, StoreSettings, CustomerInput, Customer, TallyConnector } from '@tallyui/core';
2
2
  export { ConnectorUnauthorizedError } from '@tallyui/core';
3
3
  import { RxJsonSchema } from 'rxdb';
4
4
 
@@ -101,6 +101,19 @@ declare function vendureGlobalStockSettings(context: SyncContext): Promise<{
101
101
  outOfStockThreshold: number;
102
102
  }>;
103
103
 
104
+ declare function toVendureCustomer(raw: {
105
+ id: string;
106
+ firstName?: string | null;
107
+ lastName?: string | null;
108
+ emailAddress?: string | null;
109
+ phoneNumber?: string | null;
110
+ }): Customer;
111
+ declare function searchVendureCustomers(context: SyncContext, query: string, options?: {
112
+ limit?: number;
113
+ }): Promise<Customer[]>;
114
+ declare function createVendureCustomer(context: SyncContext, input: CustomerInput): Promise<Customer>;
115
+ declare function getVendureCustomer(context: SyncContext, id: string): Promise<Customer | null>;
116
+
104
117
  /**
105
118
  * Vendure connector for Tally UI. Build one per store session, anew on each sign-in or store change:
106
119
  * each instance owns its reconcile feed, whose queued work must never reach another store's database (#307).
@@ -138,4 +151,4 @@ declare const createVendureConnector: (options?: {
138
151
  */
139
152
  declare const vendureConnector: TallyConnector;
140
153
 
141
- export { VendureTimezoneConfigError, createVendureConnector, createVendureProductReplication, createVendureVariantFeedReplication, vendureAuth, vendureConnector, vendureGlobalStockSettings, vendureProductReplication, vendureProductSchema, vendureProductSync, vendureProductTraits, vendureSignIn, vendureStockReconcile, vendureStoreSettings };
154
+ export { VendureTimezoneConfigError, createVendureConnector, createVendureCustomer, createVendureProductReplication, createVendureVariantFeedReplication, getVendureCustomer, searchVendureCustomers, toVendureCustomer, vendureAuth, vendureConnector, vendureGlobalStockSettings, vendureProductReplication, vendureProductSchema, vendureProductSync, vendureProductTraits, vendureSignIn, vendureStockReconcile, vendureStoreSettings };
package/dist/index.js CHANGED
@@ -871,6 +871,79 @@ var vendureStoreSettings = async (context, _choice) => {
871
871
  };
872
872
  };
873
873
 
874
+ // src/customers.ts
875
+ import { ConnectorUnauthorizedError as ConnectorUnauthorizedError5, CustomerServiceError } from "@tallyui/core";
876
+ var WALK_IN_EMAIL = "walk-in@vendurepos.local";
877
+ var SEARCH_QUERY = `query Search($q: String!, $take: Int!) {
878
+ customers(options: {
879
+ filter: { emailAddress: { contains: $q }, firstName: { contains: $q }, lastName: { contains: $q } },
880
+ filterOperator: OR, take: $take, sort: { lastName: ASC }
881
+ }) {
882
+ totalItems
883
+ items { id firstName lastName emailAddress phoneNumber }
884
+ }
885
+ }`;
886
+ var CREATE_QUERY = `mutation Create($input: CreateCustomerInput!) {
887
+ createCustomer(input: $input) {
888
+ __typename
889
+ ... on Customer { id firstName lastName emailAddress phoneNumber }
890
+ ... on ErrorResult { errorCode message }
891
+ }
892
+ }`;
893
+ var GET_QUERY = `query One($id: ID!) {
894
+ customer(id: $id) { id firstName lastName emailAddress phoneNumber }
895
+ }`;
896
+ function toVendureCustomer(raw) {
897
+ const customer = { id: raw.id, name: raw.id };
898
+ for (const [key, value] of [
899
+ ["firstName", raw.firstName],
900
+ ["lastName", raw.lastName],
901
+ ["email", raw.emailAddress],
902
+ ["phone", raw.phoneNumber]
903
+ ]) {
904
+ if (typeof value === "string" && value.length > 0) customer[key] = value;
905
+ }
906
+ customer.name = [customer.firstName, customer.lastName].filter(Boolean).join(" ") || customer.email || customer.id;
907
+ return customer;
908
+ }
909
+ async function searchVendureCustomers(context, query2, options) {
910
+ const q = query2.trim();
911
+ if (!q) return [];
912
+ const take = Math.max(1, Math.min(50, options?.limit ?? 20));
913
+ const body = await customerGql(context, SEARCH_QUERY, { q, take });
914
+ const items = body?.data?.customers?.items;
915
+ if (!Array.isArray(items)) throw new CustomerServiceError("server", "unexpected response");
916
+ return items.filter((raw) => raw.emailAddress !== WALK_IN_EMAIL).map(toVendureCustomer);
917
+ }
918
+ async function createVendureCustomer(context, input) {
919
+ const variables = { input: {
920
+ emailAddress: input.email,
921
+ firstName: input.firstName ?? "",
922
+ lastName: input.lastName ?? "",
923
+ ...input.phone ? { phoneNumber: input.phone } : {}
924
+ } };
925
+ const body = await customerGql(context, CREATE_QUERY, variables);
926
+ const result = body?.data?.createCustomer;
927
+ if (result?.__typename === "Customer") return toVendureCustomer(result);
928
+ if (result?.__typename && result.message) throw new CustomerServiceError("invalid", result.message);
929
+ throw new CustomerServiceError("server", "unexpected response");
930
+ }
931
+ async function customerGql(context, query2, variables) {
932
+ try {
933
+ return await gql2(context, query2, variables);
934
+ } catch (error) {
935
+ if (error instanceof ConnectorUnauthorizedError5) throw error;
936
+ throw new CustomerServiceError(error instanceof Error && error.message.startsWith("Vendure ") ? "server" : "network", error instanceof Error ? error.message : String(error));
937
+ }
938
+ }
939
+ async function getVendureCustomer(context, id) {
940
+ const body = await customerGql(context, GET_QUERY, { id });
941
+ const raw = body?.data?.customer;
942
+ if (raw === null) return null;
943
+ if (typeof raw?.id !== "string") throw new CustomerServiceError("server", "unexpected response");
944
+ return toVendureCustomer(raw);
945
+ }
946
+
874
947
  // src/global-settings.ts
875
948
  var GLOBAL_SETTINGS_QUERY = `
876
949
  query GlobalStockSettings {
@@ -883,7 +956,7 @@ async function vendureGlobalStockSettings(context) {
883
956
  }
884
957
 
885
958
  // src/index.ts
886
- import { ConnectorUnauthorizedError as ConnectorUnauthorizedError5 } from "@tallyui/core";
959
+ import { ConnectorUnauthorizedError as ConnectorUnauthorizedError6 } from "@tallyui/core";
887
960
  var VENDURE_IDS_PER_REQUEST = 1e3;
888
961
  var createVendureConnector = (options = {}) => {
889
962
  const idFeed = createReconcileFeed({ fetchByIds: createFetchByIds(options.barcodeField) });
@@ -923,17 +996,25 @@ var createVendureConnector = (options = {}) => {
923
996
  prices: { fetchPages: fetchPages2, fingerprint, enqueue: idFeed.enqueue, refetchBatchSize: VENDURE_IDS_PER_REQUEST }
924
997
  },
925
998
  storeSettings: vendureStoreSettings,
999
+ // Needs the administrator's ReadCustomer permission (and CreateCustomer to create).
1000
+ searchCustomers: searchVendureCustomers,
1001
+ createCustomer: createVendureCustomer,
1002
+ getCustomer: getVendureCustomer,
926
1003
  // Re-reads a restored session's capabilities; with an API key and no sign-in, the only reader.
927
1004
  capabilities: (context) => readVendureCapabilities(context.baseUrl, context.headers, { signal: context.signal })
928
1005
  };
929
1006
  };
930
1007
  var vendureConnector = createVendureConnector();
931
1008
  export {
932
- ConnectorUnauthorizedError5 as ConnectorUnauthorizedError,
1009
+ ConnectorUnauthorizedError6 as ConnectorUnauthorizedError,
933
1010
  VendureTimezoneConfigError,
934
1011
  createVendureConnector,
1012
+ createVendureCustomer,
935
1013
  createVendureProductReplication,
936
1014
  createVendureVariantFeedReplication,
1015
+ getVendureCustomer,
1016
+ searchVendureCustomers,
1017
+ toVendureCustomer,
937
1018
  vendureAuth,
938
1019
  vendureConnector,
939
1020
  vendureGlobalStockSettings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/connector-vendure",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
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.2"
29
+ "@tallyui/core": "^3.0.4"
30
30
  },
31
31
  "devDependencies": {
32
- "@tallyui/database": "3.0.2",
33
- "@tallyui/pos": "3.0.2",
32
+ "@tallyui/database": "3.0.4",
33
+ "@tallyui/pos": "3.0.4",
34
34
  "rxdb": "17.5.0"
35
35
  },
36
36
  "scripts": {
@@ -0,0 +1,43 @@
1
+ {
2
+ "query": "query Search($q: String!, $take: Int!) {\n customers(options: {\n filter: { emailAddress: { contains: $q }, firstName: { contains: $q }, lastName: { contains: $q } },\n filterOperator: OR, take: $take, sort: { lastName: ASC }\n }) {\n totalItems\n items { id createdAt updatedAt title firstName lastName emailAddress phoneNumber customFields }\n }\n}",
3
+ "variables": {
4
+ "q": "ada",
5
+ "take": 20
6
+ },
7
+ "httpStatus": 200,
8
+ "response": {
9
+ "data": {
10
+ "customers": {
11
+ "totalItems": 2,
12
+ "items": [
13
+ {
14
+ "id": "3",
15
+ "createdAt": "2026-10-06T01:13:57.519Z",
16
+ "updatedAt": "2026-10-06T01:13:57.519Z",
17
+ "title": null,
18
+ "firstName": "Adam",
19
+ "lastName": "Fixture",
20
+ "emailAddress": "adam.fixture@example.com",
21
+ "phoneNumber": null,
22
+ "customFields": {
23
+ "__entityId__": 3
24
+ }
25
+ },
26
+ {
27
+ "id": "2",
28
+ "createdAt": "2026-10-06T01:13:57.496Z",
29
+ "updatedAt": "2026-10-06T01:13:57.496Z",
30
+ "title": null,
31
+ "firstName": "Ada",
32
+ "lastName": "Lovelace",
33
+ "emailAddress": "ada.fixture@example.com",
34
+ "phoneNumber": "+44 20 7946 0001",
35
+ "customFields": {
36
+ "__entityId__": 2
37
+ }
38
+ }
39
+ ]
40
+ }
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "query": "query Search($q: String!, $take: Int!) {\n customers(options: {\n filter: { emailAddress: { contains: $q }, firstName: { contains: $q }, lastName: { contains: $q } },\n filterOperator: OR, take: $take, sort: { lastName: ASC }\n }) {\n totalItems\n items { id createdAt updatedAt title firstName lastName emailAddress phoneNumber customFields }\n }\n}",
3
+ "variables": {
4
+ "q": "zzqqxx",
5
+ "take": 20
6
+ },
7
+ "httpStatus": 200,
8
+ "response": {
9
+ "data": {
10
+ "customers": {
11
+ "totalItems": 0,
12
+ "items": []
13
+ }
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "query": "mutation Create($input: CreateCustomerInput!) {\n createCustomer(input: $input) {\n __typename\n ... on Customer { id createdAt updatedAt title firstName lastName emailAddress phoneNumber customFields }\n ... on ErrorResult { errorCode message }\n }\n}",
3
+ "variables": {
4
+ "input": {
5
+ "firstName": "Ada",
6
+ "lastName": "Lovelace",
7
+ "emailAddress": "ada.fixture@example.com",
8
+ "phoneNumber": "+44 20 7946 0001"
9
+ }
10
+ },
11
+ "httpStatus": 200,
12
+ "response": {
13
+ "data": {
14
+ "createCustomer": {
15
+ "__typename": "Customer",
16
+ "id": "2",
17
+ "createdAt": "2026-10-06T01:13:57.496Z",
18
+ "updatedAt": "2026-10-06T01:13:57.496Z",
19
+ "title": null,
20
+ "firstName": "Ada",
21
+ "lastName": "Lovelace",
22
+ "emailAddress": "ada.fixture@example.com",
23
+ "phoneNumber": "+44 20 7946 0001",
24
+ "customFields": {
25
+ "__entityId__": 2
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "query": "mutation Create($input: CreateCustomerInput!) {\n createCustomer(input: $input) {\n __typename\n ... on Customer { id createdAt updatedAt title firstName lastName emailAddress phoneNumber customFields }\n ... on ErrorResult { errorCode message }\n }\n}",
3
+ "variables": {
4
+ "input": {
5
+ "firstName": "Ada",
6
+ "lastName": "Again",
7
+ "emailAddress": "ada.fixture@example.com"
8
+ }
9
+ },
10
+ "httpStatus": 200,
11
+ "response": {
12
+ "data": {
13
+ "createCustomer": {
14
+ "__typename": "EmailAddressConflictError",
15
+ "errorCode": "EMAIL_ADDRESS_CONFLICT_ERROR",
16
+ "message": "The email address is not available."
17
+ }
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "query": "query One($id: ID!) {\n customer(id: $id) {\n id createdAt updatedAt title firstName lastName emailAddress phoneNumber customFields\n addresses { id fullName company streetLine1 streetLine2 city province postalCode country { code name } phoneNumber defaultShippingAddress defaultBillingAddress }\n user { id identifier verified }\n groups { id name }\n }\n}",
3
+ "variables": {
4
+ "id": "2"
5
+ },
6
+ "httpStatus": 200,
7
+ "response": {
8
+ "data": {
9
+ "customer": {
10
+ "id": "2",
11
+ "createdAt": "2026-10-06T01:13:57.496Z",
12
+ "updatedAt": "2026-10-06T01:13:57.496Z",
13
+ "title": null,
14
+ "firstName": "Ada",
15
+ "lastName": "Lovelace",
16
+ "emailAddress": "ada.fixture@example.com",
17
+ "phoneNumber": "+44 20 7946 0001",
18
+ "customFields": {
19
+ "__entityId__": 2
20
+ },
21
+ "addresses": [
22
+ {
23
+ "id": "1",
24
+ "fullName": "Ada Lovelace",
25
+ "company": "",
26
+ "streetLine1": "12 Fixture Street",
27
+ "streetLine2": "",
28
+ "city": "London",
29
+ "province": "",
30
+ "postalCode": "N1 1AA",
31
+ "country": {
32
+ "code": "DE",
33
+ "name": "Germany"
34
+ },
35
+ "phoneNumber": "",
36
+ "defaultShippingAddress": true,
37
+ "defaultBillingAddress": true
38
+ }
39
+ ],
40
+ "user": {
41
+ "id": "2",
42
+ "identifier": "ada.fixture@example.com",
43
+ "verified": false
44
+ },
45
+ "groups": []
46
+ }
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,76 @@
1
+ import { ConnectorUnauthorizedError, CustomerServiceError, type Customer, type CustomerInput, type SyncContext } from '@tallyui/core';
2
+ import { gql } from './replication/products';
3
+
4
+ // The vendurepos plugin's walk-in customer is never a person to attach to a sale.
5
+ const WALK_IN_EMAIL = 'walk-in@vendurepos.local';
6
+
7
+ const SEARCH_QUERY = `query Search($q: String!, $take: Int!) {
8
+ customers(options: {
9
+ filter: { emailAddress: { contains: $q }, firstName: { contains: $q }, lastName: { contains: $q } },
10
+ filterOperator: OR, take: $take, sort: { lastName: ASC }
11
+ }) {
12
+ totalItems
13
+ items { id firstName lastName emailAddress phoneNumber }
14
+ }
15
+ }`;
16
+ const CREATE_QUERY = `mutation Create($input: CreateCustomerInput!) {
17
+ createCustomer(input: $input) {
18
+ __typename
19
+ ... on Customer { id firstName lastName emailAddress phoneNumber }
20
+ ... on ErrorResult { errorCode message }
21
+ }
22
+ }`;
23
+ const GET_QUERY = `query One($id: ID!) {
24
+ customer(id: $id) { id firstName lastName emailAddress phoneNumber }
25
+ }`;
26
+
27
+ export function toVendureCustomer(raw: { id: string; firstName?: string | null; lastName?: string | null; emailAddress?: string | null; phoneNumber?: string | null }): Customer {
28
+ const customer: Customer = { id: raw.id, name: raw.id };
29
+ for (const [key, value] of [
30
+ ['firstName', raw.firstName], ['lastName', raw.lastName],
31
+ ['email', raw.emailAddress], ['phone', raw.phoneNumber],
32
+ ] as const) {
33
+ if (typeof value === 'string' && value.length > 0) customer[key] = value;
34
+ }
35
+ customer.name = [customer.firstName, customer.lastName].filter(Boolean).join(' ') || customer.email || customer.id;
36
+ return customer;
37
+ }
38
+
39
+ export async function searchVendureCustomers(context: SyncContext, query: string, options?: { limit?: number }): Promise<Customer[]> {
40
+ const q = query.trim();
41
+ if (!q) return [];
42
+ const take = Math.max(1, Math.min(50, options?.limit ?? 20));
43
+ const body = await customerGql(context, SEARCH_QUERY, { q, take });
44
+ const items = body?.data?.customers?.items;
45
+ if (!Array.isArray(items)) throw new CustomerServiceError('server', 'unexpected response');
46
+ return items.filter((raw) => raw.emailAddress !== WALK_IN_EMAIL).map(toVendureCustomer);
47
+ }
48
+
49
+ export async function createVendureCustomer(context: SyncContext, input: CustomerInput): Promise<Customer> {
50
+ const variables = { input: {
51
+ emailAddress: input.email, firstName: input.firstName ?? '', lastName: input.lastName ?? '',
52
+ ...(input.phone ? { phoneNumber: input.phone } : {}),
53
+ } };
54
+ const body = await customerGql(context, CREATE_QUERY, variables);
55
+ const result = body?.data?.createCustomer;
56
+ if (result?.__typename === 'Customer') return toVendureCustomer(result);
57
+ if (result?.__typename && result.message) throw new CustomerServiceError('invalid', result.message);
58
+ throw new CustomerServiceError('server', 'unexpected response');
59
+ }
60
+
61
+ async function customerGql(context: SyncContext, query: string, variables: Record<string, unknown>): Promise<any> {
62
+ try {
63
+ return await gql(context, query, variables);
64
+ } catch (error) {
65
+ if (error instanceof ConnectorUnauthorizedError) throw error;
66
+ throw new CustomerServiceError(error instanceof Error && error.message.startsWith('Vendure ') ? 'server' : 'network', error instanceof Error ? error.message : String(error));
67
+ }
68
+ }
69
+
70
+ export async function getVendureCustomer(context: SyncContext, id: string): Promise<Customer | null> {
71
+ const body = await customerGql(context, GET_QUERY, { id });
72
+ const raw = body?.data?.customer;
73
+ if (raw === null) return null;
74
+ if (typeof raw?.id !== 'string') throw new CustomerServiceError('server', 'unexpected response');
75
+ return toVendureCustomer(raw);
76
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import { createFetchByIds, fetchPages, variantIds } from './reconcile/ids';
12
12
  import { fetchPages as fetchPricePages, fingerprint as priceFingerprint } from './reconcile/prices';
13
13
  import { vendureStoreSettings } from './store-settings';
14
14
  import { vendureGlobalStockSettings } from './global-settings';
15
+ import { searchVendureCustomers, createVendureCustomer, getVendureCustomer } from './customers';
15
16
 
16
17
  /** Ids per fetchByIds request: it sends the reconcile feed's whole chunk (at most 1,000 ids, ADR-060) in one query. */
17
18
  const VENDURE_IDS_PER_REQUEST = 1000;
@@ -86,6 +87,11 @@ export const createVendureConnector = (options: {
86
87
 
87
88
  storeSettings: vendureStoreSettings,
88
89
 
90
+ // Needs the administrator's ReadCustomer permission (and CreateCustomer to create).
91
+ searchCustomers: searchVendureCustomers,
92
+ createCustomer: createVendureCustomer,
93
+ getCustomer: getVendureCustomer,
94
+
89
95
  // Re-reads a restored session's capabilities; with an API key and no sign-in, the only reader.
90
96
  capabilities: (context) => readVendureCapabilities(context.baseUrl, context.headers, { signal: context.signal }),
91
97
  };
@@ -107,6 +113,7 @@ export { createVendureVariantFeedReplication } from './replication/variant-feed'
107
113
  export { vendureStockReconcile } from './reconcile/stock';
108
114
  export { vendureStoreSettings } from './store-settings';
109
115
  export { vendureGlobalStockSettings } from './global-settings';
116
+ export { searchVendureCustomers, createVendureCustomer, getVendureCustomer, toVendureCustomer } from './customers';
110
117
 
111
118
  export { ConnectorUnauthorizedError } from '@tallyui/core';
112
119
  export { VendureTimezoneConfigError } from './replication/products';