@flowselections/floriday-klanten-module 1.0.15 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist-lib/assets/floriday-logo.png.asset.json +11 -0
  2. package/dist-lib/components/klanten/BackLink.d.ts +12 -0
  3. package/dist-lib/components/klanten/BackLink.d.ts.map +1 -0
  4. package/dist-lib/components/klanten/BackLink.js +12 -0
  5. package/dist-lib/components/klanten/FloridayAssortmentTab.d.ts +4 -0
  6. package/dist-lib/components/klanten/FloridayAssortmentTab.d.ts.map +1 -0
  7. package/dist-lib/components/klanten/FloridayAssortmentTab.js +80 -0
  8. package/dist-lib/components/klanten/FloridayDeeplinkButton.d.ts +8 -0
  9. package/dist-lib/components/klanten/FloridayDeeplinkButton.d.ts.map +1 -0
  10. package/dist-lib/components/klanten/FloridayDeeplinkButton.js +9 -0
  11. package/dist-lib/components/klanten/GroupedKlantDetail.d.ts.map +1 -1
  12. package/dist-lib/components/klanten/GroupedKlantDetail.js +19 -5
  13. package/dist-lib/components/klanten/ImportKlanten.d.ts.map +1 -1
  14. package/dist-lib/components/klanten/ImportKlanten.js +3 -2
  15. package/dist-lib/components/klanten/KlantDetail.d.ts.map +1 -1
  16. package/dist-lib/components/klanten/KlantDetail.js +9 -3
  17. package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
  18. package/dist-lib/components/klanten/KlantenOverzicht.js +6 -3
  19. package/dist-lib/components/klanten/NetwerkPagina.d.ts.map +1 -1
  20. package/dist-lib/components/klanten/NetwerkPagina.js +21 -18
  21. package/dist-lib/components/klanten/NieuweKlant.d.ts.map +1 -1
  22. package/dist-lib/components/klanten/NieuweKlant.js +23 -8
  23. package/dist-lib/components/klanten/PriceGroupBadges.d.ts +7 -0
  24. package/dist-lib/components/klanten/PriceGroupBadges.d.ts.map +1 -0
  25. package/dist-lib/components/klanten/PriceGroupBadges.js +13 -0
  26. package/dist-lib/components/klanten/SyncStatusChip.d.ts +4 -0
  27. package/dist-lib/components/klanten/SyncStatusChip.d.ts.map +1 -0
  28. package/dist-lib/components/klanten/SyncStatusChip.js +49 -0
  29. package/dist-lib/components/klanten/pricegroups/CustomerPickerDialog.d.ts +8 -0
  30. package/dist-lib/components/klanten/pricegroups/CustomerPickerDialog.d.ts.map +1 -0
  31. package/dist-lib/components/klanten/pricegroups/CustomerPickerDialog.js +75 -0
  32. package/dist-lib/components/klanten/pricegroups/PriceGroupFormPage.d.ts +4 -0
  33. package/dist-lib/components/klanten/pricegroups/PriceGroupFormPage.d.ts.map +1 -0
  34. package/dist-lib/components/klanten/pricegroups/PriceGroupFormPage.js +269 -0
  35. package/dist-lib/components/klanten/pricegroups/PriceGroupsPage.d.ts +2 -0
  36. package/dist-lib/components/klanten/pricegroups/PriceGroupsPage.d.ts.map +1 -0
  37. package/dist-lib/components/klanten/pricegroups/PriceGroupsPage.js +133 -0
  38. package/dist-lib/components/klanten/pricegroups/ProductPickerDialog.d.ts +8 -0
  39. package/dist-lib/components/klanten/pricegroups/ProductPickerDialog.d.ts.map +1 -0
  40. package/dist-lib/components/klanten/pricegroups/ProductPickerDialog.js +18 -0
  41. package/dist-lib/index.d.ts +1 -0
  42. package/dist-lib/index.d.ts.map +1 -1
  43. package/dist-lib/index.js +6 -1
  44. package/dist-lib/integrations/supabase/auth-middleware.d.ts +762 -3
  45. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  46. package/dist-lib/integrations/supabase/client.d.ts +762 -3
  47. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  48. package/dist-lib/integrations/supabase/client.js +4 -3
  49. package/dist-lib/integrations/supabase/client.server.d.ts +762 -3
  50. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  51. package/dist-lib/integrations/supabase/types.d.ts +789 -8
  52. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  53. package/dist-lib/lib/accounting/twinfield/client.server.d.ts +21 -0
  54. package/dist-lib/lib/accounting/twinfield/client.server.d.ts.map +1 -0
  55. package/dist-lib/lib/accounting/twinfield/client.server.js +171 -0
  56. package/dist-lib/lib/accounting/twinfield/mapping.d.ts +46 -0
  57. package/dist-lib/lib/accounting/twinfield/mapping.d.ts.map +1 -0
  58. package/dist-lib/lib/accounting/twinfield/mapping.js +109 -0
  59. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +36318 -0
  60. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -0
  61. package/dist-lib/lib/accounting/twinfield/worker.functions.js +48 -0
  62. package/dist-lib/lib/accounting/twinfield/worker.server.d.ts +12 -0
  63. package/dist-lib/lib/accounting/twinfield/worker.server.d.ts.map +1 -0
  64. package/dist-lib/lib/accounting/twinfield/worker.server.js +124 -0
  65. package/dist-lib/lib/accounting.functions.d.ts +2286 -9
  66. package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
  67. package/dist-lib/lib/customer-analytics.functions.d.ts +762 -3
  68. package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
  69. package/dist-lib/lib/customer-schemas.d.ts +2 -0
  70. package/dist-lib/lib/customer-schemas.d.ts.map +1 -1
  71. package/dist-lib/lib/customer-schemas.js +1 -0
  72. package/dist-lib/lib/customers.functions.d.ts +18241 -99
  73. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  74. package/dist-lib/lib/customers.functions.js +92 -4
  75. package/dist-lib/lib/delivery-locations.functions.d.ts +3048 -12
  76. package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
  77. package/dist-lib/lib/external-customers.functions.d.ts +3048 -12
  78. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
  79. package/dist-lib/lib/floriday-assortment.functions.d.ts +12127 -0
  80. package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -0
  81. package/dist-lib/lib/floriday-assortment.functions.js +213 -0
  82. package/dist-lib/lib/floriday-discounts.functions.d.ts +1800 -282
  83. package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
  84. package/dist-lib/lib/floriday-trade-items.functions.d.ts +12111 -0
  85. package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -0
  86. package/dist-lib/lib/floriday-trade-items.functions.js +107 -0
  87. package/dist-lib/lib/local-discounts.functions.d.ts +5336 -23
  88. package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
  89. package/dist-lib/lib/overdue-invoices.functions.d.ts +762 -3
  90. package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
  91. package/dist-lib/lib/price-groups-local.functions.d.ts +84841 -0
  92. package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -0
  93. package/dist-lib/lib/price-groups-local.functions.js +148 -0
  94. package/dist-lib/lib/price-groups.functions.d.ts +12120 -0
  95. package/dist-lib/lib/price-groups.functions.d.ts.map +1 -0
  96. package/dist-lib/lib/price-groups.functions.js +115 -0
  97. package/dist-lib/lib/pricing.d.ts +24 -0
  98. package/dist-lib/lib/pricing.d.ts.map +1 -0
  99. package/dist-lib/lib/pricing.js +60 -0
  100. package/package.json +6 -3
@@ -0,0 +1,21 @@
1
+ export interface TwinfieldConnection {
2
+ id: string;
3
+ source: "connections" | "credentials";
4
+ client_id: string | null;
5
+ client_secret: string | null;
6
+ refresh_token: string | null;
7
+ access_token: string | null;
8
+ token_expires_at: string | null;
9
+ cluster_uri: string | null;
10
+ company_code: string | null;
11
+ office_code: string | null;
12
+ }
13
+ export declare function loadActiveConnection(preferredId?: string): Promise<TwinfieldConnection | null>;
14
+ export declare function ensureAccessToken(conn: TwinfieldConnection): Promise<string>;
15
+ export declare function ensureCluster(conn: TwinfieldConnection, accessToken: string): Promise<string>;
16
+ export declare function sendProcessXml(conn: TwinfieldConnection, dimensionXml: string): Promise<{
17
+ ok: boolean;
18
+ message: string;
19
+ raw: string;
20
+ }>;
21
+ //# sourceMappingURL=client.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.server.d.ts","sourceRoot":"","sources":["../../../../src/lib/accounting/twinfield/client.server.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,aAAa,GAAG,aAAa,CAAC;IACtC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAKD,wBAAsB,oBAAoB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CA0DpG;AA8BD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6BlF;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAenG;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAyC5I"}
@@ -0,0 +1,171 @@
1
+ import { supabaseAdmin } from "../../../integrations/supabase/client.server";
2
+ import { buildProcessXmlEnvelope } from "./mapping";
3
+ const TOKEN_URL = "https://login.twinfield.com/auth/authentication/connect/token";
4
+ const VALIDATE_URL = "https://login.twinfield.com/auth/authentication/connect/accesstokenvalidation";
5
+ export async function loadActiveConnection(preferredId) {
6
+ // Voorkeur: expliciete connection uit twinfield_connections
7
+ if (preferredId) {
8
+ const { data } = await supabaseAdmin
9
+ .from("twinfield_connections")
10
+ .select("id, client_id, client_secret, refresh_token, access_token, token_expires_at, cluster_uri, company_code, office_code, is_active")
11
+ .eq("id", preferredId)
12
+ .maybeSingle();
13
+ if (data && data.is_active) {
14
+ return {
15
+ id: data.id,
16
+ source: "connections",
17
+ client_id: data.client_id,
18
+ client_secret: data.client_secret,
19
+ refresh_token: data.refresh_token,
20
+ access_token: data.access_token,
21
+ token_expires_at: data.token_expires_at,
22
+ cluster_uri: data.cluster_uri,
23
+ company_code: data.company_code,
24
+ office_code: data.office_code,
25
+ };
26
+ }
27
+ }
28
+ const { data: conn } = await supabaseAdmin
29
+ .from("twinfield_connections")
30
+ .select("id, client_id, client_secret, refresh_token, access_token, token_expires_at, cluster_uri, company_code, office_code")
31
+ .eq("is_active", true)
32
+ .order("connected_at", { ascending: false, nullsFirst: false })
33
+ .limit(1)
34
+ .maybeSingle();
35
+ if (conn) {
36
+ return { ...conn, source: "connections" };
37
+ }
38
+ const { data: cred } = await supabaseAdmin
39
+ .from("twinfield_credentials")
40
+ .select("id, client_id, client_secret, refresh_token, cluster, administration_code")
41
+ .not("refresh_token", "is", null)
42
+ .order("updated_at", { ascending: false })
43
+ .limit(1)
44
+ .maybeSingle();
45
+ if (cred) {
46
+ return {
47
+ id: cred.id,
48
+ source: "credentials",
49
+ client_id: cred.client_id,
50
+ client_secret: cred.client_secret,
51
+ refresh_token: cred.refresh_token,
52
+ access_token: null,
53
+ token_expires_at: null,
54
+ cluster_uri: cred.cluster,
55
+ company_code: cred.administration_code,
56
+ office_code: cred.administration_code,
57
+ };
58
+ }
59
+ return null;
60
+ }
61
+ async function persistNewToken(conn, token) {
62
+ const expiresAt = new Date(Date.now() + (token.expires_in - 30) * 1000).toISOString();
63
+ const table = conn.source === "connections" ? "twinfield_connections" : "twinfield_credentials";
64
+ const patch = { updated_at: new Date().toISOString() };
65
+ if (conn.source === "connections") {
66
+ patch.access_token = token.access_token;
67
+ patch.token_expires_at = expiresAt;
68
+ if (token.refresh_token)
69
+ patch.refresh_token = token.refresh_token;
70
+ }
71
+ else {
72
+ // credentials-tabel heeft geen access_token kolom; alleen refresh bijwerken
73
+ if (token.refresh_token)
74
+ patch.refresh_token = token.refresh_token;
75
+ }
76
+ await supabaseAdmin.from(table).update(patch).eq("id", conn.id);
77
+ await supabaseAdmin
78
+ .from("twinfield_token_refresh_log")
79
+ .insert({ connection_id: conn.id, success: true })
80
+ .then(() => { }, () => { });
81
+ conn.access_token = token.access_token;
82
+ conn.token_expires_at = expiresAt;
83
+ if (token.refresh_token)
84
+ conn.refresh_token = token.refresh_token;
85
+ }
86
+ export async function ensureAccessToken(conn) {
87
+ if (conn.access_token &&
88
+ conn.token_expires_at &&
89
+ new Date(conn.token_expires_at).getTime() > Date.now() + 60000) {
90
+ return conn.access_token;
91
+ }
92
+ if (!conn.refresh_token || !conn.client_id || !conn.client_secret) {
93
+ throw new Error("Twinfield-koppeling is niet volledig geconfigureerd (client_id/secret/refresh_token ontbreekt).");
94
+ }
95
+ const body = new URLSearchParams({
96
+ grant_type: "refresh_token",
97
+ client_id: conn.client_id,
98
+ client_secret: conn.client_secret,
99
+ refresh_token: conn.refresh_token,
100
+ });
101
+ const res = await fetch(TOKEN_URL, {
102
+ method: "POST",
103
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
104
+ body,
105
+ });
106
+ if (!res.ok) {
107
+ const text = await res.text().catch(() => "");
108
+ throw new Error(`Twinfield token refresh mislukt (${res.status}): ${text.slice(0, 200)}`);
109
+ }
110
+ const json = (await res.json());
111
+ await persistNewToken(conn, json);
112
+ return json.access_token;
113
+ }
114
+ export async function ensureCluster(conn, accessToken) {
115
+ if (conn.cluster_uri && conn.cluster_uri.startsWith("http"))
116
+ return conn.cluster_uri.replace(/\/+$/, "");
117
+ const res = await fetch(`${VALIDATE_URL}?token=${encodeURIComponent(accessToken)}`);
118
+ if (!res.ok)
119
+ throw new Error(`Twinfield cluster-lookup faalde (${res.status})`);
120
+ const json = (await res.json());
121
+ const cluster = json.twf_cluster || json.cluster;
122
+ if (!cluster)
123
+ throw new Error("Twinfield cluster-URL niet gevonden in validation-response");
124
+ if (conn.source === "connections") {
125
+ await supabaseAdmin
126
+ .from("twinfield_connections")
127
+ .update({ cluster_uri: cluster, updated_at: new Date().toISOString() })
128
+ .eq("id", conn.id);
129
+ }
130
+ conn.cluster_uri = cluster;
131
+ return cluster.replace(/\/+$/, "");
132
+ }
133
+ export async function sendProcessXml(conn, dimensionXml) {
134
+ const companyCode = conn.office_code || conn.company_code;
135
+ if (!companyCode)
136
+ throw new Error("Twinfield office/company code ontbreekt in de koppeling.");
137
+ let token = await ensureAccessToken(conn);
138
+ let cluster = await ensureCluster(conn, token);
139
+ const url = `${cluster}/webservices/processxml.asmx`;
140
+ const doCall = async () => {
141
+ const envelope = buildProcessXmlEnvelope(token, companyCode, dimensionXml);
142
+ return fetch(url, {
143
+ method: "POST",
144
+ headers: {
145
+ "Content-Type": "text/xml; charset=utf-8",
146
+ SOAPAction: "http://www.twinfield.com/ProcessXmlDocument",
147
+ },
148
+ body: envelope,
149
+ });
150
+ };
151
+ let res = await doCall();
152
+ if (res.status === 401) {
153
+ // Forceer refresh
154
+ conn.token_expires_at = null;
155
+ conn.access_token = null;
156
+ token = await ensureAccessToken(conn);
157
+ cluster = await ensureCluster(conn, token);
158
+ res = await doCall();
159
+ }
160
+ const text = await res.text();
161
+ if (!res.ok) {
162
+ throw new Error(`Twinfield SOAP-fout ${res.status}: ${text.slice(0, 400)}`);
163
+ }
164
+ // Zeer eenvoudige result-parse: zoek result="1" of msg="1" in de body
165
+ const okMatch = /result="1"/.test(text);
166
+ const msgMatch = /<message[^>]*>([^<]+)<\/message>/i.exec(text);
167
+ if (!okMatch && /msg="1"/.test(text)) {
168
+ throw new Error(`Twinfield weigerde de klant: ${msgMatch?.[1] ?? "onbekende fout"}`);
169
+ }
170
+ return { ok: true, message: msgMatch?.[1] ?? "OK", raw: text };
171
+ }
@@ -0,0 +1,46 @@
1
+ export interface TwinfieldCustomerInput {
2
+ id: string;
3
+ twinfield_code: string;
4
+ company_name: string;
5
+ contact_person?: string | null;
6
+ email?: string | null;
7
+ billing_email?: string | null;
8
+ phone?: string | null;
9
+ fax?: string | null;
10
+ website?: string | null;
11
+ street?: string | null;
12
+ house_number?: string | null;
13
+ postal_code?: string | null;
14
+ city?: string | null;
15
+ country?: string | null;
16
+ address_line1?: string | null;
17
+ address_line2?: string | null;
18
+ vat_number?: string | null;
19
+ coc_number?: string | null;
20
+ payment_term_days?: number | null;
21
+ discount_days?: number | null;
22
+ discount_percentage?: number | null;
23
+ bank_iban?: string | null;
24
+ bank_bic?: string | null;
25
+ bank_account_holder?: string | null;
26
+ bank_account_number?: string | null;
27
+ bank_name?: string | null;
28
+ bank_address_line1?: string | null;
29
+ bank_house_number?: string | null;
30
+ bank_postal_code?: string | null;
31
+ bank_city?: string | null;
32
+ bank_country?: string | null;
33
+ language?: string | null;
34
+ is_active?: boolean | null;
35
+ }
36
+ export interface MappedCustomer {
37
+ xml: string;
38
+ hash: string;
39
+ }
40
+ /**
41
+ * Bouw Twinfield DEB-dimension XML voor processxml.asmx.
42
+ * Zie: https://developers.twinfield.com/documentation → Master data → Customers
43
+ */
44
+ export declare function buildCustomerDimensionXml(c: TwinfieldCustomerInput, office: string): MappedCustomer;
45
+ export declare function buildProcessXmlEnvelope(accessToken: string, companyCode: string, dimensionXml: string): string;
46
+ //# sourceMappingURL=mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../../src/lib/accounting/twinfield/mapping.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,CAAC,EAAE,sBAAsB,EACzB,MAAM,EAAE,MAAM,GACb,cAAc,CAyEhB;AAED,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,MAAM,CAeR"}
@@ -0,0 +1,109 @@
1
+ import { createHash } from "crypto";
2
+ function xmlEscape(v) {
3
+ if (v === null || v === undefined)
4
+ return "";
5
+ return String(v)
6
+ .replace(/&/g, "&amp;")
7
+ .replace(/</g, "&lt;")
8
+ .replace(/>/g, "&gt;")
9
+ .replace(/"/g, "&quot;")
10
+ .replace(/'/g, "&apos;");
11
+ }
12
+ function el(tag, value, attrs) {
13
+ const v = value === null || value === undefined ? "" : String(value).trim();
14
+ if (!v)
15
+ return "";
16
+ const a = attrs
17
+ ? " " + Object.entries(attrs).map(([k, vv]) => `${k}="${xmlEscape(vv)}"`).join(" ")
18
+ : "";
19
+ return `<${tag}${a}>${xmlEscape(v)}</${tag}>`;
20
+ }
21
+ /**
22
+ * Bouw Twinfield DEB-dimension XML voor processxml.asmx.
23
+ * Zie: https://developers.twinfield.com/documentation → Master data → Customers
24
+ */
25
+ export function buildCustomerDimensionXml(c, office) {
26
+ const streetLine = [c.street, c.house_number].filter(Boolean).join(" ") || c.address_line1 || "";
27
+ const address = [
28
+ `<address type="invoice" default="true">`,
29
+ el("field", "name"),
30
+ el("name", c.company_name),
31
+ el("contact", c.contact_person),
32
+ el("country", c.country),
33
+ el("city", c.city),
34
+ el("postcode", c.postal_code),
35
+ el("telephone", c.phone),
36
+ el("fax", c.fax),
37
+ el("email", c.billing_email || c.email),
38
+ el("field1", streetLine),
39
+ el("field2", c.address_line2),
40
+ "</address>",
41
+ ].filter(Boolean).join("");
42
+ const bank = c.bank_iban
43
+ ? [
44
+ `<bank default="true">`,
45
+ el("ascription", c.bank_account_holder || c.company_name),
46
+ el("accountnumber", c.bank_account_number),
47
+ el("iban", c.bank_iban),
48
+ el("bicode", c.bank_bic),
49
+ el("bankname", c.bank_name),
50
+ el("city", c.bank_city),
51
+ el("country", c.bank_country),
52
+ el("postcode", c.bank_postal_code),
53
+ el("address", [c.bank_address_line1, c.bank_house_number].filter(Boolean).join(" ")),
54
+ "</bank>",
55
+ ].filter(Boolean).join("")
56
+ : "";
57
+ const financials = [
58
+ "<financials>",
59
+ el("duedays", c.payment_term_days ?? undefined),
60
+ el("discountdays", c.discount_days ?? undefined),
61
+ el("discountpercentage", c.discount_percentage ?? undefined),
62
+ el("ebilling", "true"),
63
+ el("ebillmail", c.billing_email || c.email),
64
+ el("vatcode", c.country && c.country.toUpperCase() !== "NL" ? "VH" : "VN"),
65
+ el("substitutionlevel", "0"),
66
+ "</financials>",
67
+ ].filter(Boolean).join("");
68
+ const dimension = [
69
+ `<dimension status="active">`,
70
+ el("code", c.twinfield_code),
71
+ el("office", office),
72
+ el("type", "DEB"),
73
+ el("name", c.company_name),
74
+ el("shortname", c.company_name.slice(0, 25)),
75
+ el("website", c.website),
76
+ el("coc", c.coc_number),
77
+ el("vatnumber", c.vat_number),
78
+ el("inuse", "true"),
79
+ el("behaviour", "normal"),
80
+ el("touched", "0"),
81
+ el("beginperiod", "0"),
82
+ el("beginyear", "0"),
83
+ el("endperiod", "0"),
84
+ el("endyear", "0"),
85
+ el("editdimensionname", "true"),
86
+ financials,
87
+ `<addresses>${address}</addresses>`,
88
+ bank ? `<banks>${bank}</banks>` : "",
89
+ "</dimension>",
90
+ ].filter(Boolean).join("");
91
+ const hash = createHash("sha1").update(dimension).digest("hex");
92
+ return { xml: dimension, hash };
93
+ }
94
+ export function buildProcessXmlEnvelope(accessToken, companyCode, dimensionXml) {
95
+ return `<?xml version="1.0" encoding="utf-8"?>
96
+ <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
97
+ <soap:Header>
98
+ <Header xmlns="http://www.twinfield.com/">
99
+ <AccessToken>${xmlEscape(accessToken)}</AccessToken>
100
+ <CompanyCode>${xmlEscape(companyCode)}</CompanyCode>
101
+ </Header>
102
+ </soap:Header>
103
+ <soap:Body>
104
+ <ProcessXmlDocument xmlns="http://www.twinfield.com/">
105
+ <xmlRequest>${dimensionXml}</xmlRequest>
106
+ </ProcessXmlDocument>
107
+ </soap:Body>
108
+ </soap:Envelope>`;
109
+ }