@flowselections/floriday-afhandeling-module 1.0.63 → 1.0.64

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.
@@ -0,0 +1,100 @@
1
+ export interface SignupCronStatus {
2
+ cronAvailable: boolean;
3
+ active: boolean;
4
+ enabled: boolean;
5
+ reason: string | null;
6
+ timezone: string | null;
7
+ lastRun: {
8
+ started_at: string;
9
+ ok: boolean;
10
+ sent: number;
11
+ reason: string | null;
12
+ error: string | null;
13
+ } | null;
14
+ sentToday: number;
15
+ healed: boolean;
16
+ syncCode: string | null;
17
+ }
18
+ /** Leest de status; herstelt de taak (alleen beheerder) als hij ontbreekt terwijl de mail aan staat. */
19
+ export declare const getSignupCronStatus: import("@tanstack/start-client-core").OptionalFetcher<readonly [import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
20
+ supabase: import("../integrations/supabase/auth-middleware").AuthedSupabaseClient;
21
+ userId: string;
22
+ claims: import("@supabase/auth-js").JwtPayload;
23
+ }, undefined, undefined, undefined>], (d: {
24
+ sync?: boolean;
25
+ } | undefined) => {
26
+ sync: boolean;
27
+ }, Promise<SignupCronStatus>>;
28
+ export interface SignupGroup {
29
+ carrier_id: string;
30
+ carrier_name: string;
31
+ pickup_date: string;
32
+ pickup_time: string;
33
+ signup_time: string;
34
+ pickup_moment_key: string;
35
+ signup_deadline: string;
36
+ pickup_at: string;
37
+ send_at: string;
38
+ order_count: number;
39
+ cart_count: number;
40
+ last: {
41
+ sent_at: string;
42
+ cart_count: number;
43
+ version: number;
44
+ fingerprint_match: boolean;
45
+ } | null;
46
+ }
47
+ export interface SignupOverview {
48
+ ok: boolean;
49
+ enabled: boolean;
50
+ reason: string | null;
51
+ timezone: string | null;
52
+ groups: SignupGroup[];
53
+ unassigned: {
54
+ orderId: string;
55
+ orderNumber: string | null;
56
+ deliveryDate: string;
57
+ }[];
58
+ }
59
+ export declare const getSignupOverview: import("@tanstack/start-client-core").RequiredFetcher<readonly [import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
60
+ supabase: import("../integrations/supabase/auth-middleware").AuthedSupabaseClient;
61
+ userId: string;
62
+ claims: import("@supabase/auth-js").JwtPayload;
63
+ }, undefined, undefined, undefined>], (d: {
64
+ from: string;
65
+ to: string;
66
+ }) => {
67
+ from: string;
68
+ to: string;
69
+ }, Promise<SignupOverview>>;
70
+ interface GroupKey {
71
+ carrierId: string;
72
+ pickupDate: string;
73
+ momentKey: string;
74
+ }
75
+ export interface SignupPreview {
76
+ ok: boolean;
77
+ reason: string | null;
78
+ to: string | null;
79
+ subject: string;
80
+ text: string;
81
+ html: string;
82
+ cart_count: number;
83
+ already_sent: boolean;
84
+ kind: "initial" | "change";
85
+ }
86
+ export declare const previewSignupMail: import("@tanstack/start-client-core").RequiredFetcher<readonly [import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
87
+ supabase: import("../integrations/supabase/auth-middleware").AuthedSupabaseClient;
88
+ userId: string;
89
+ claims: import("@supabase/auth-js").JwtPayload;
90
+ }, undefined, undefined, undefined>], (d: GroupKey) => GroupKey, Promise<SignupPreview>>;
91
+ export declare const sendSignupMailNow: import("@tanstack/start-client-core").RequiredFetcher<readonly [import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
92
+ supabase: import("../integrations/supabase/auth-middleware").AuthedSupabaseClient;
93
+ userId: string;
94
+ claims: import("@supabase/auth-js").JwtPayload;
95
+ }, undefined, undefined, undefined>], (d: GroupKey) => GroupKey, Promise<{
96
+ ok: boolean;
97
+ code: string;
98
+ }>>;
99
+ export {};
100
+ //# sourceMappingURL=carrierSignup.functions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"carrierSignup.functions.d.ts","sourceRoot":"","sources":["../../src/lib/carrierSignup.functions.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/G,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAkCD,wGAAwG;AACxG,eAAO,MAAM,mBAAmB;;;;0CAEV;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS;;6BAchD,CAAC;AAEL,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACnG;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,UAAU,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACrF;AAED,eAAO,MAAM,iBAAiB;;;;0CAER;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE;UAApB,MAAM;QAAM,MAAM;2BAkB5C,CAAC;AAEL,UAAU,QAAQ;IAAG,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE;AAQ/E,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAC;CAC5B;AAED,eAAO,MAAM,iBAAiB;;;;0CAnBN,QAAQ,sCAwC5B,CAAC;AAEL,eAAO,MAAM,iBAAiB;;;;0CA1CN,QAAQ;QA6CoB,OAAO;UAAQ,MAAM;GAQrE,CAAC"}
@@ -0,0 +1,117 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+ import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware";
3
+ async function isBeheerder(supabase, userId) {
4
+ for (const role of ["admin", "superadmin"]) {
5
+ const { data } = await supabase.rpc("has_role", { _user_id: userId, _role: role });
6
+ if (data === true)
7
+ return true;
8
+ }
9
+ return false;
10
+ }
11
+ async function runSync() {
12
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
13
+ const admin = supabaseAdmin;
14
+ const { data, error } = await admin.rpc("afhandeling_sync_signup_cron");
15
+ if (error)
16
+ throw new Error(`Automatische controle kon niet worden ingesteld: ${error.message}`);
17
+ const r = (data ?? {});
18
+ return { ok: r.ok === true, code: String(r.code ?? "unknown") };
19
+ }
20
+ async function readStatus(supabase) {
21
+ const { data, error } = await supabase.rpc("afhandeling_signup_cron_status");
22
+ if (error)
23
+ throw new Error(error.message);
24
+ const r = (data ?? {});
25
+ return {
26
+ cronAvailable: r.cron_available === true,
27
+ active: r.active === true,
28
+ enabled: r.enabled === true,
29
+ reason: r.reason ?? null,
30
+ timezone: r.timezone ?? null,
31
+ lastRun: r.last_run ?? null,
32
+ sentToday: Number(r.sent_today ?? 0),
33
+ };
34
+ }
35
+ /** Leest de status; herstelt de taak (alleen beheerder) als hij ontbreekt terwijl de mail aan staat. */
36
+ export const getSignupCronStatus = createServerFn({ method: "POST" })
37
+ .middleware([requireSupabaseAuth])
38
+ .inputValidator((d) => ({ sync: Boolean(d?.sync) }))
39
+ .handler(async ({ data, context }) => {
40
+ const supabase = context.supabase;
41
+ let status = await readStatus(supabase);
42
+ let healed = false;
43
+ let syncCode = null;
44
+ const needsSync = data.sync || (status.cronAvailable && status.enabled !== status.active);
45
+ if (needsSync && status.cronAvailable && (await isBeheerder(supabase, context.userId))) {
46
+ const r = await runSync();
47
+ syncCode = r.code;
48
+ healed = !data.sync && r.code === "created";
49
+ status = await readStatus(supabase);
50
+ }
51
+ return { ...status, healed, syncCode };
52
+ });
53
+ export const getSignupOverview = createServerFn({ method: "POST" })
54
+ .middleware([requireSupabaseAuth])
55
+ .inputValidator((d) => {
56
+ const re = /^\d{4}-\d{2}-\d{2}$/;
57
+ if (!re.test(d.from) || !re.test(d.to))
58
+ throw new Error("Ongeldige datum");
59
+ return d;
60
+ })
61
+ .handler(async ({ data, context }) => {
62
+ const supabase = context.supabase;
63
+ const { data: r, error } = await supabase.rpc("carrier_signup_overview", { p_from: data.from, p_to: data.to });
64
+ if (error)
65
+ throw new Error(error.message);
66
+ const o = (r ?? {});
67
+ return {
68
+ ok: o.ok === true,
69
+ enabled: o.enabled === true,
70
+ reason: o.reason ?? null,
71
+ timezone: o.timezone ?? null,
72
+ groups: Array.isArray(o.groups) ? o.groups : [],
73
+ unassigned: Array.isArray(o.unassigned) ? o.unassigned : [],
74
+ };
75
+ });
76
+ const validateKey = (d) => {
77
+ if (!/^[0-9a-f-]{36}$/i.test(d.carrierId) || !/^\d{4}-\d{2}-\d{2}$/.test(d.pickupDate) || !d.momentKey || d.momentKey.length > 80) {
78
+ throw new Error("Ongeldig ophaalmoment");
79
+ }
80
+ return d;
81
+ };
82
+ export const previewSignupMail = createServerFn({ method: "POST" })
83
+ .middleware([requireSupabaseAuth])
84
+ .inputValidator(validateKey)
85
+ .handler(async ({ data, context }) => {
86
+ const supabase = context.supabase;
87
+ const { data: r, error } = await supabase.rpc("carrier_signup_preview", {
88
+ p_carrier_id: data.carrierId, p_pickup_date: data.pickupDate, p_moment_key: data.momentKey,
89
+ });
90
+ if (error)
91
+ throw new Error(error.message);
92
+ const o = (r ?? {});
93
+ return {
94
+ ok: o.ok === true,
95
+ reason: o.reason ?? null,
96
+ to: o.to ?? null,
97
+ subject: String(o.subject ?? ""),
98
+ text: String(o.text ?? ""),
99
+ html: String(o.html ?? ""),
100
+ cart_count: Number(o.cart_count ?? 0),
101
+ already_sent: o.already_sent === true,
102
+ kind: o.kind === "change" ? "change" : "initial",
103
+ };
104
+ });
105
+ export const sendSignupMailNow = createServerFn({ method: "POST" })
106
+ .middleware([requireSupabaseAuth])
107
+ .inputValidator(validateKey)
108
+ .handler(async ({ data, context }) => {
109
+ const supabase = context.supabase;
110
+ const { data: r, error } = await supabase.rpc("carrier_signup_send_now", {
111
+ p_carrier_id: data.carrierId, p_pickup_date: data.pickupDate, p_moment_key: data.momentKey,
112
+ });
113
+ if (error)
114
+ throw new Error(error.message);
115
+ const o = (r ?? {});
116
+ return { ok: o.ok === true, code: String(o.code ?? "unknown") };
117
+ });
@@ -1,3 +1,3 @@
1
1
  import type { SupabaseClient } from "@supabase/supabase-js";
2
- export declare function handleDispatchCarrierSignups(request: Request, injected?: SupabaseClient): Promise<Response>;
2
+ export declare function handleDispatchCarrierSignups(_request: Request, _injected?: SupabaseClient): Promise<Response>;
3
3
  //# sourceMappingURL=dispatchCarrierSignups.handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dispatchCarrierSignups.handler.d.ts","sourceRoot":"","sources":["../../../src/lib/serverRoutes/dispatchCarrierSignups.handler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AA+G5D,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,OAAO,EAChB,QAAQ,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,QAAQ,CAAC,CAgVnB"}
1
+ {"version":3,"file":"dispatchCarrierSignups.handler.d.ts","sourceRoot":"","sources":["../../../src/lib/serverRoutes/dispatchCarrierSignups.handler.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,OAAO,EACjB,SAAS,CAAC,EAAE,cAAc,GACzB,OAAO,CAAC,QAAQ,CAAC,CAKnB"}
@@ -1,326 +1,3 @@
1
- import { DEFAULT_PICKUP_MOMENT, getSignupDeadline, selectPickupMoment, subtractWorkdays, } from "../planning";
2
- import { buildCarrierSignupMail } from "../carrierSignupMail.server";
3
- import { pickCarrierForAddress } from "../carrierMatch.server";
4
- function normalizeMoments(raw) {
5
- if (!Array.isArray(raw) || raw.length === 0)
6
- return [DEFAULT_PICKUP_MOMENT];
7
- return raw.map((m) => {
8
- const r = (m ?? {});
9
- return {
10
- signup_days: Number(r.signup_days ?? DEFAULT_PICKUP_MOMENT.signup_days),
11
- signup_time: String(r.signup_time ?? DEFAULT_PICKUP_MOMENT.signup_time),
12
- pickup_days: Number(r.pickup_days ?? DEFAULT_PICKUP_MOMENT.pickup_days),
13
- pickup_time: String(r.pickup_time ?? DEFAULT_PICKUP_MOMENT.pickup_time),
14
- delivery_days: Number(r.delivery_days ?? DEFAULT_PICKUP_MOMENT.delivery_days),
15
- delivery_time: String(r.delivery_time ?? DEFAULT_PICKUP_MOMENT.delivery_time),
16
- };
17
- });
18
- }
19
- function momentKey(m) {
20
- return `${m.pickup_days}@${m.pickup_time}|${m.signup_days}@${m.signup_time}`;
21
- }
22
- function ymd(d) {
23
- const y = d.getFullYear();
24
- const mo = String(d.getMonth() + 1).padStart(2, "0");
25
- const da = String(d.getDate()).padStart(2, "0");
26
- return `${y}-${mo}-${da}`;
27
- }
28
- function cartsFor(items) {
29
- let carts = 0;
30
- let plants = 0;
31
- for (const it of items ?? []) {
32
- const qty = Number(it.quantity ?? 0);
33
- if (!Number.isFinite(qty) || qty <= 0)
34
- continue;
35
- plants += qty;
36
- const p = Array.isArray(it.products) ? it.products[0] : it.products;
37
- const perCart = p && p.plants_per_crate && p.crates_per_layer && p.layers_per_cart
38
- ? Number(p.plants_per_crate) * Number(p.crates_per_layer) * Number(p.layers_per_cart)
39
- : 0;
40
- if (perCart > 0)
41
- carts += Math.ceil(qty / perCart);
42
- }
43
- return { carts, plants };
44
- }
45
- export async function handleDispatchCarrierSignups(request, injected) {
46
- const apikey = request.headers.get("apikey") ?? request.headers.get("x-api-key");
47
- const expected = process.env.SUPABASE_PUBLISHABLE_KEY;
48
- if (!expected || !apikey || apikey !== expected) {
49
- return new Response("Unauthorized", { status: 401 });
50
- }
51
- let admin;
52
- if (injected) {
53
- admin = injected;
54
- }
55
- else {
56
- const { supabaseAdmin } = await import("../../integrations/supabase/client.server");
57
- admin = supabaseAdmin;
58
- }
59
- const startedAt = new Date();
60
- const logRun = async (payload) => {
61
- try {
62
- await admin.from("carrier_signup_dispatch_runs").insert({
63
- started_at: startedAt.toISOString(),
64
- finished_at: new Date().toISOString(),
65
- ok: payload.ok,
66
- sent: payload.sent ?? 0,
67
- groups: payload.groups ?? 0,
68
- skipped: payload.skipped ?? [],
69
- unassigned: payload.unassigned ?? [],
70
- reason: payload.reason ?? null,
71
- error: payload.error ?? null,
72
- detail: payload.detail ?? null,
73
- });
74
- }
75
- catch (e) {
76
- console.error("[dispatch-carrier-signups] run-log insert failed", e);
77
- }
78
- };
79
- try {
80
- const { data: settingsRows, error: settingsErr } = await admin
81
- .from("afhandeling_settings")
82
- .select("signup_mail_enabled, signup_mail_lead_minutes, signup_mail_purpose")
83
- .eq("signup_mail_enabled", true)
84
- .limit(1);
85
- if (settingsErr) {
86
- await logRun({ ok: false, reason: "settings_error", error: settingsErr.message });
87
- return Response.json({ ok: false, error: settingsErr.message }, { status: 500 });
88
- }
89
- const settings = settingsRows?.[0] ?? null;
90
- if (!settings) {
91
- await logRun({ ok: true, reason: "mail_disabled" });
92
- return Response.json({ ok: true, skipped: "disabled", sent: 0 });
93
- }
94
- const leadMin = settings.signup_mail_lead_minutes ?? 60;
95
- const purpose = settings.signup_mail_purpose ?? "afhandeling_carrier_signup";
96
- const { data: purposeConnections, error: purposeErr } = await admin.rpc("email_auth_for_purpose", {
97
- _direction: "send",
98
- _purpose: purpose,
99
- });
100
- if (purposeErr)
101
- throw purposeErr;
102
- const connectionId = (purposeConnections ?? []).find((c) => c?.needs_reauth !== true && c?.status === "connected")?.id ?? null;
103
- if (!connectionId) {
104
- await logRun({
105
- ok: false,
106
- sent: 0,
107
- groups: 0,
108
- reason: "no_active_send_connection",
109
- error: `Geen actief send-adres voor doeleinde ${purpose}`,
110
- detail: { purpose },
111
- });
112
- return new Response(JSON.stringify({ ok: false, reason: "no_active_send_connection", purpose }), { status: 500, headers: { "content-type": "application/json" } });
113
- }
114
- const { data: allCarriers, error: carriersErr } = await admin
115
- .from("carriers")
116
- .select("id, name, email, is_default, sort_order, service_countries, service_provinces, service_cities, pickup_moments, pickup_weekdays, delivery_weekdays");
117
- if (carriersErr) {
118
- await logRun({ ok: false, reason: "carriers_error", error: carriersErr.message });
119
- return Response.json({ ok: false, error: carriersErr.message }, { status: 500 });
120
- }
121
- const carriers = (allCarriers ?? []).filter((c) => !!c.email);
122
- if (carriers.length === 0) {
123
- await logRun({ ok: true, reason: "no_carrier_with_email" });
124
- return Response.json({ ok: true, skipped: "no_carrier_with_email", sent: 0 });
125
- }
126
- const now = new Date();
127
- const horizon = new Date(now);
128
- horizon.setDate(horizon.getDate() + 14);
129
- const { data: orders } = await admin
130
- .from("orders")
131
- .select("id, order_number, customer_name, customer_id, order_date, delivery_date, notes, created_at, custom_fields, order_items(product_name, quantity, unit, products(plants_per_crate, crates_per_layer, layers_per_cart, location))")
132
- .is("deleted_at", null)
133
- .gte("delivery_date", ymd(now))
134
- .lte("delivery_date", ymd(horizon))
135
- .limit(2000);
136
- const rawOrders = orders ?? [];
137
- const customerIds = Array.from(new Set(rawOrders.map((o) => o.customer_id).filter((v) => !!v)));
138
- const dlIds = Array.from(new Set(rawOrders
139
- .map((o) => o.custom_fields?.delivery_location ?? null)
140
- .filter((v) => !!v)));
141
- const customerMap = new Map();
142
- if (customerIds.length > 0) {
143
- const { data: cs } = await admin
144
- .from("customers")
145
- .select("id, company_name, email, phone, contact_person, external_id")
146
- .in("id", customerIds);
147
- for (const c of cs ?? []) {
148
- customerMap.set(c.id, c);
149
- }
150
- }
151
- const dlMap = new Map();
152
- if (dlIds.length > 0) {
153
- const { data: ds } = await admin
154
- .from("customer_delivery_locations")
155
- .select("id, name, street, house_number, postal_code, city, province, country")
156
- .in("id", dlIds);
157
- for (const d of ds ?? []) {
158
- dlMap.set(d.id, d);
159
- }
160
- }
161
- const groups = new Map();
162
- const unassigned = [];
163
- for (const raw of rawOrders) {
164
- if (!raw.delivery_date)
165
- continue;
166
- const delivery = new Date(raw.delivery_date);
167
- if (Number.isNaN(delivery.getTime()))
168
- continue;
169
- const dlId = raw.custom_fields?.delivery_location ?? null;
170
- const dl = dlId ? dlMap.get(dlId) : undefined;
171
- const carrier = pickCarrierForAddress(carriers, {
172
- country: dl?.country ?? null,
173
- province: dl?.province ?? null,
174
- city: dl?.city ?? null,
175
- });
176
- if (!carrier) {
177
- unassigned.push(raw.order_number ?? raw.id);
178
- continue;
179
- }
180
- const moments = normalizeMoments(carrier.pickup_moments);
181
- const created = raw.created_at ? new Date(raw.created_at) : now;
182
- const moment = selectPickupMoment(delivery, created, moments);
183
- const pickupDate = subtractWorkdays(delivery, moment.pickup_days);
184
- const signupDeadline = getSignupDeadline(delivery, moment);
185
- const key = `${carrier.id}::${ymd(pickupDate)}::${momentKey(moment)}`;
186
- const { carts, plants } = cartsFor(raw.order_items);
187
- const cust = raw.customer_id ? customerMap.get(raw.customer_id) : undefined;
188
- const pickupLocations = Array.from(new Set((raw.order_items ?? [])
189
- .map((it) => {
190
- const p = Array.isArray(it.products) ? it.products[0] : it.products;
191
- return p?.location ?? null;
192
- })
193
- .filter((v) => !!v && v.trim() !== "")));
194
- const row = {
195
- orderNumber: raw.order_number ?? "—",
196
- orderDate: raw.order_date,
197
- deliveryDate: raw.delivery_date,
198
- deliveryType: "Mailbox",
199
- notes: raw.notes,
200
- cartCount: carts || null,
201
- plantCount: plants || null,
202
- customer: {
203
- name: cust?.company_name ?? raw.customer_name ?? "—",
204
- contactPerson: cust?.contact_person ?? null,
205
- email: cust?.email ?? null,
206
- phone: cust?.phone ?? null,
207
- customerNumber: cust?.external_id ?? null,
208
- },
209
- deliveryLocation: dl
210
- ? {
211
- name: dl.name,
212
- street: dl.street,
213
- houseNumber: dl.house_number,
214
- postalCode: dl.postal_code,
215
- city: dl.city,
216
- province: dl.province,
217
- country: dl.country,
218
- }
219
- : null,
220
- pickupLocations,
221
- items: (raw.order_items ?? []).map((it) => ({
222
- name: it.product_name ?? "—",
223
- quantity: it.quantity === null ? null : Number(it.quantity),
224
- unit: it.unit,
225
- })),
226
- };
227
- const g = groups.get(key);
228
- if (g) {
229
- g.rows.push(row);
230
- g.totalCarts += carts;
231
- }
232
- else {
233
- groups.set(key, {
234
- carrier,
235
- pickupDate,
236
- moment,
237
- signupDeadline,
238
- rows: [row],
239
- totalCarts: carts,
240
- });
241
- }
242
- }
243
- const leadMs = leadMin * 60 * 1000;
244
- let sent = 0;
245
- const skipped = [];
246
- for (const [key, g] of groups) {
247
- const triggerAt = g.signupDeadline.getTime() - leadMs;
248
- if (now.getTime() < triggerAt)
249
- continue;
250
- if (now.getTime() > g.signupDeadline.getTime() + 72 * 60 * 60 * 1000)
251
- continue;
252
- const pickupDateKey = ymd(g.pickupDate);
253
- const mKey = momentKey(g.moment);
254
- const { data: existing } = await admin
255
- .from("carrier_signup_dispatches")
256
- .select("id")
257
- .eq("carrier_id", g.carrier.id)
258
- .eq("pickup_date", pickupDateKey)
259
- .eq("pickup_moment_key", mKey)
260
- .maybeSingle();
261
- if (existing) {
262
- skipped.push(`${key}=already`);
263
- continue;
264
- }
265
- const mail = buildCarrierSignupMail({
266
- carrierName: g.carrier.name,
267
- pickupDate: g.pickupDate,
268
- pickupTime: g.moment.pickup_time,
269
- signupDeadline: g.signupDeadline,
270
- orders: g.rows,
271
- });
272
- const { data: outbox, error: outErr } = await admin
273
- .from("email_auth_outbox")
274
- .insert({
275
- purpose,
276
- connection_id: connectionId,
277
- to_addresses: [g.carrier.email],
278
- cc_addresses: [],
279
- bcc_addresses: [],
280
- subject: mail.subject,
281
- html_body: mail.html,
282
- text_body: mail.text,
283
- headers: {},
284
- scheduled_at: new Date().toISOString(),
285
- status: "queued",
286
- })
287
- .select("id")
288
- .single();
289
- if (outErr) {
290
- skipped.push(`${key}=outbox_err:${outErr.message}`);
291
- continue;
292
- }
293
- await admin.from("carrier_signup_dispatches").insert({
294
- carrier_id: g.carrier.id,
295
- pickup_date: pickupDateKey,
296
- pickup_moment_key: mKey,
297
- signup_deadline: g.signupDeadline.toISOString(),
298
- order_count: g.rows.length,
299
- cart_count: mail.totalCarts,
300
- outbox_id: outbox?.id ?? null,
301
- });
302
- sent++;
303
- }
304
- await logRun({
305
- ok: true,
306
- sent,
307
- groups: groups.size,
308
- skipped,
309
- unassigned,
310
- reason: sent === 0 ? "no_group_in_window" : null,
311
- });
312
- return Response.json({
313
- ok: true,
314
- sent,
315
- groups: groups.size,
316
- skipped,
317
- unassigned,
318
- });
319
- }
320
- catch (e) {
321
- const msg = e instanceof Error ? e.message : String(e);
322
- console.error("[dispatch-carrier-signups] unhandled error", e);
323
- await logRun({ ok: false, reason: "unhandled_exception", error: msg });
324
- return Response.json({ ok: false, error: msg }, { status: 500 });
325
- }
1
+ export async function handleDispatchCarrierSignups(_request, _injected) {
2
+ return Response.json({ ok: false, code: "gone", error: "De aanmeldmail wordt door de database zelf verstuurd; deze route is vervallen." }, { status: 410 });
326
3
  }
@@ -0,0 +1,4 @@
1
+ export declare function signupReasonText(reason: string | null | undefined): string | null;
2
+ export declare function fmtTime(iso: string | null | undefined, tz: string | null): string;
3
+ export declare function fmtDayTime(iso: string | null | undefined, tz: string | null): string;
4
+ //# sourceMappingURL=signupMailText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signupMailText.d.ts","sourceRoot":"","sources":["../../src/lib/signupMailText.ts"],"names":[],"mappings":"AACA,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAuBjF;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAGjF;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAGpF"}
@@ -0,0 +1,35 @@
1
+ // Gebruikersteksten voor de aanmeldmail (geen techniek).
2
+ export function signupReasonText(reason) {
3
+ switch (reason) {
4
+ case null:
5
+ case undefined:
6
+ case "":
7
+ return null;
8
+ case "no_send_connection":
9
+ return "Er is nog geen verzendadres gekoppeld aan ‘Aanmeldmail vervoerders’. Koppel er een bij E-mailkoppeling.";
10
+ case "send_connection_needs_reauth":
11
+ return "Het verzendadres voor ‘Aanmeldmail vervoerders’ moet opnieuw worden gekoppeld bij E-mailkoppeling.";
12
+ case "no_timezone":
13
+ return "De tijdzone van de vestiging ontbreekt. Stel die in bij Werktijden; tot dan gaan er geen aanmeldmails weg.";
14
+ case "mail_disabled":
15
+ return "De aanmeldmail staat uit.";
16
+ case "group_not_found":
17
+ return "Dit ophaalmoment heeft geen orders meer.";
18
+ case "after_pickup":
19
+ return "De ophaaltijd is al voorbij.";
20
+ case "already_sent":
21
+ return "Deze stand is al verstuurd.";
22
+ default:
23
+ return "De aanmeldmail kon niet worden gecontroleerd.";
24
+ }
25
+ }
26
+ export function fmtTime(iso, tz) {
27
+ if (!iso || !tz)
28
+ return "—";
29
+ return new Intl.DateTimeFormat("nl-NL", { timeZone: tz, hour: "2-digit", minute: "2-digit" }).format(new Date(iso));
30
+ }
31
+ export function fmtDayTime(iso, tz) {
32
+ if (!iso || !tz)
33
+ return "—";
34
+ return new Intl.DateTimeFormat("nl-NL", { timeZone: tz, weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }).format(new Date(iso));
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/floriday-afhandeling-module",
3
- "version": "1.0.63",
3
+ "version": "1.0.64",
4
4
  "private": false,
5
5
  "sideEffects": false,
6
6
  "type": "module",