@flowselections/google-maps-api-installatie-techniek 1.0.4 → 1.1.1

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 (37) hide show
  1. package/README.md +33 -0
  2. package/dist-lib/components/MapsBeheerPage.d.ts.map +1 -1
  3. package/dist-lib/components/MapsBeheerPage.js +27 -2
  4. package/dist-lib/components/maps/MapsDiagnosticsPanel.js +1 -1
  5. package/dist-lib/components/maps/MapsErrorsPanel.d.ts +2 -0
  6. package/dist-lib/components/maps/MapsErrorsPanel.d.ts.map +1 -0
  7. package/dist-lib/components/maps/MapsErrorsPanel.js +26 -0
  8. package/dist-lib/components/maps/MapsStatusPanel.d.ts +1 -1
  9. package/dist-lib/components/maps/MapsStatusPanel.js +1 -1
  10. package/dist-lib/components/settings/MapsSettingsCard.js +1 -1
  11. package/dist-lib/integrations/supabase/client.js +1 -1
  12. package/dist-lib/integrations/supabase/client.server.js +1 -1
  13. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +11 -5
  14. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
  15. package/dist-lib/integrations/supabase/previewAuthStorage.js +59 -24
  16. package/dist-lib/integrations/supabase/types.d.ts +23505 -6704
  17. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  18. package/dist-lib/integrations/supabase/types.js +2 -0
  19. package/dist-lib/lib/maps-admin.server.d.ts +9 -1
  20. package/dist-lib/lib/maps-admin.server.d.ts.map +1 -1
  21. package/dist-lib/lib/maps-admin.server.js +60 -6
  22. package/dist-lib/lib/maps-errors.d.ts +24 -0
  23. package/dist-lib/lib/maps-errors.d.ts.map +1 -0
  24. package/dist-lib/lib/maps-errors.js +57 -0
  25. package/dist-lib/lib/maps-errors.test.d.ts +2 -0
  26. package/dist-lib/lib/maps-errors.test.d.ts.map +1 -0
  27. package/dist-lib/lib/maps-errors.test.js +40 -0
  28. package/dist-lib/lib/maps-processor.server.d.ts +2 -0
  29. package/dist-lib/lib/maps-processor.server.d.ts.map +1 -1
  30. package/dist-lib/lib/maps-processor.server.js +117 -22
  31. package/dist-lib/lib/maps-processor.test.d.ts +2 -0
  32. package/dist-lib/lib/maps-processor.test.d.ts.map +1 -0
  33. package/dist-lib/lib/maps-processor.test.js +182 -0
  34. package/dist-lib/lib/maps.functions.d.ts +52 -8
  35. package/dist-lib/lib/maps.functions.d.ts.map +1 -1
  36. package/dist-lib/lib/maps.functions.js +24 -1
  37. package/package.json +8 -6
@@ -39,6 +39,8 @@ export const Constants = {
39
39
  "veilingbon",
40
40
  "afleverbrief",
41
41
  "veilingbrief",
42
+ "traysticker",
43
+ "eigen_sticker",
42
44
  ],
43
45
  doc_orientation: ["portrait", "landscape"],
44
46
  exact_env: ["staging", "live"],
@@ -1,10 +1,17 @@
1
- import type { MapsOverview } from "./maps.functions";
1
+ import type { MapsErrorPage, MapsOverview } from "./maps.functions";
2
2
  export declare function getMapsOverviewData(): Promise<MapsOverview>;
3
3
  export declare function saveMapsSettings(input: {
4
4
  cacheTtlDays: number;
5
5
  dailyCallLimit: number;
6
6
  allowEstimateFallback: boolean;
7
7
  provider: "free" | "google";
8
+ claimViaRpcEnabled?: boolean;
9
+ perCallLimitEnabled?: boolean;
10
+ maxAttemptsEnabled?: boolean;
11
+ stopBijSleutelfoutEnabled?: boolean;
12
+ maxAttempts?: number;
13
+ timezone?: string;
14
+ expectedUpdatedAt?: string | null;
8
15
  }): Promise<{
9
16
  ok: boolean;
10
17
  }>;
@@ -14,4 +21,5 @@ export declare function clearCache(): Promise<{
14
21
  export declare function purgeExpired(): Promise<{
15
22
  ok: boolean;
16
23
  }>;
24
+ export declare function listMapsErrors(page: number, pageSize: number): Promise<MapsErrorPage>;
17
25
  //# sourceMappingURL=maps-admin.server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"maps-admin.server.d.ts","sourceRoot":"","sources":["../../src/lib/maps-admin.server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAiCrD,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,YAAY,CAAC,CAuEjE;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC7B;;GAeA;AAED,wBAAsB,UAAU;;GAK/B;AAED,wBAAsB,YAAY;;GAQjC"}
1
+ {"version":3,"file":"maps-admin.server.d.ts","sourceRoot":"","sources":["../../src/lib/maps-admin.server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAkCpE,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,YAAY,CAAC,CA8EjE;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC5B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;;GA8BA;AAED,wBAAsB,UAAU;;GAK/B;AAED,wBAAsB,YAAY;;GAQjC;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA0B3F"}
@@ -1,6 +1,7 @@
1
1
  // Server-only beheerlogica voor de kaarten-module.
2
2
  import { createClient } from "@supabase/supabase-js";
3
3
  import { getGoogleConnectionStatus } from "./central-auth.server";
4
+ import { classifyGoogleError, resolveProcessorSettings } from "./maps-errors";
4
5
  // Ruwe schatting: Routes/Places kosten ~ USD 5 per 1000 elementen.
5
6
  // Alleen relevant zodra de betaalde Google-provider actief is.
6
7
  const COST_PER_ELEMENT = 0.005;
@@ -66,6 +67,13 @@ export async function getMapsOverviewData() {
66
67
  dailyCallLimit: settings.daily_call_limit,
67
68
  allowEstimateFallback: settings.allow_estimate_fallback,
68
69
  provider: settings.provider === "google" ? "google" : "free",
70
+ claimViaRpcEnabled: settings.claim_via_rpc_enabled === true,
71
+ perCallLimitEnabled: settings.per_call_limit_enabled === true,
72
+ maxAttemptsEnabled: settings.max_attempts_enabled === true,
73
+ stopBijSleutelfoutEnabled: settings.stop_bij_sleutelfout_enabled === true,
74
+ maxAttempts: resolveProcessorSettings({ ...settings, max_attempts_enabled: true }).maxAttempts,
75
+ timezone: resolveProcessorSettings(settings).timezone,
76
+ updatedAt: settings.updated_at,
69
77
  lastSuccessAt: settings.last_success_at,
70
78
  lastErrorAt: settings.last_error_at,
71
79
  lastError: settings.last_error,
@@ -89,18 +97,37 @@ export async function getMapsOverviewData() {
89
97
  export async function saveMapsSettings(input) {
90
98
  const db = adminClient();
91
99
  const settings = await ensureSettings(db);
92
- const { error } = await db
93
- .from("maps_settings")
94
- .update({
100
+ const patch = {
95
101
  cache_ttl_days: input.cacheTtlDays,
96
102
  daily_call_limit: input.dailyCallLimit,
97
103
  allow_estimate_fallback: input.allowEstimateFallback,
98
104
  provider: input.provider,
99
105
  updated_at: new Date().toISOString(),
100
- })
101
- .eq("id", settings.id);
102
- if (error)
106
+ };
107
+ if (input.claimViaRpcEnabled !== undefined)
108
+ patch.claim_via_rpc_enabled = input.claimViaRpcEnabled;
109
+ if (input.perCallLimitEnabled !== undefined)
110
+ patch.per_call_limit_enabled = input.perCallLimitEnabled;
111
+ if (input.maxAttemptsEnabled !== undefined)
112
+ patch.max_attempts_enabled = input.maxAttemptsEnabled;
113
+ if (input.stopBijSleutelfoutEnabled !== undefined)
114
+ patch.stop_bij_sleutelfout_enabled = input.stopBijSleutelfoutEnabled;
115
+ if (input.maxAttempts !== undefined)
116
+ patch.max_attempts = input.maxAttempts;
117
+ if (input.timezone !== undefined)
118
+ patch.timezone = input.timezone;
119
+ let query = db.from("maps_settings").update(patch).eq("id", settings.id);
120
+ // Optimistische vergrendeling op updated_at.
121
+ if (input.expectedUpdatedAt)
122
+ query = query.eq("updated_at", input.expectedUpdatedAt);
123
+ const { data, error } = await query.select("id");
124
+ if (error) {
125
+ console.error("[maps] instellingen opslaan", error);
103
126
  throw new Error(error.message);
127
+ }
128
+ if (input.expectedUpdatedAt && (!Array.isArray(data) || data.length === 0)) {
129
+ throw new Error("Dit is intussen door iemand anders gewijzigd. Ververs de pagina.");
130
+ }
104
131
  return { ok: true };
105
132
  }
106
133
  export async function clearCache() {
@@ -119,3 +146,30 @@ export async function purgeExpired() {
119
146
  throw new Error(error.message);
120
147
  return { ok: true };
121
148
  }
149
+ export async function listMapsErrors(page, pageSize) {
150
+ const db = adminClient();
151
+ const from = page * pageSize;
152
+ const { data, count: total, error } = await db
153
+ .from("maps_usage_log")
154
+ .select("id, call_type, outcome, detail, created_at", { count: "exact" })
155
+ .in("outcome", ["error", "no_result"])
156
+ .order("created_at", { ascending: false })
157
+ .range(from, from + pageSize - 1);
158
+ if (error) {
159
+ console.error("[maps] fouten lezen", error);
160
+ throw new Error(error.message);
161
+ }
162
+ const rows = Array.isArray(data) ? data : [];
163
+ return {
164
+ total: total ?? 0,
165
+ page,
166
+ pageSize,
167
+ items: rows.map((row) => ({
168
+ id: String(row.id),
169
+ at: row.created_at,
170
+ callType: row.call_type,
171
+ kind: row.outcome === "no_result" ? "not_found" : classifyGoogleError(row.detail),
172
+ message: row.detail ?? (row.outcome === "no_result" ? "Adres niet gevonden" : "Onbekende fout"),
173
+ })),
174
+ };
175
+ }
@@ -0,0 +1,24 @@
1
+ export type GoogleErrorKind = "quota" | "invalid_key" | "not_found" | "token" | "other";
2
+ /** Deelt een foutmelding in. Volgorde is bewust: token vóór sleutel. */
3
+ export declare function classifyGoogleError(message: string | null | undefined): GoogleErrorKind;
4
+ /** Fouten waarbij doorgaan zinloos is (alleen relevant als de schakelaar aan staat). */
5
+ export declare function isFatalKeyError(kind: GoogleErrorKind): boolean;
6
+ export declare const DEFAULT_MAX_ATTEMPTS = 3;
7
+ export declare const DEFAULT_DAILY_LIMIT = 2000;
8
+ export declare const DEFAULT_TIMEZONE = "Europe/Amsterdam";
9
+ export interface ProcessorSettings {
10
+ dailyCallLimit: number;
11
+ maxAttempts: number;
12
+ claimViaRpc: boolean;
13
+ perCallLimit: boolean;
14
+ stopOnKeyError: boolean;
15
+ timezone: string;
16
+ }
17
+ /** Ontbrekende waarden vallen terug op het huidige gedrag (alle schakelaars uit). */
18
+ export declare function resolveProcessorSettings(row: Record<string, unknown> | null | undefined): ProcessorSettings;
19
+ /** Kalenderdatum (yyyy-mm-dd) in de opgegeven tijdzone. */
20
+ export declare function calendarDay(date: Date, timeZone: string): string;
21
+ export declare class DailyLimitReachedError extends Error {
22
+ constructor();
23
+ }
24
+ //# sourceMappingURL=maps-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maps-errors.d.ts","sourceRoot":"","sources":["../../src/lib/maps-errors.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,CAAC;AAExF,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe,CAUvF;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAE9D;AAED,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qFAAqF;AACrF,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,CAa3G;AAED,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUhE;AAED,qBAAa,sBAAuB,SAAQ,KAAK;;CAKhD"}
@@ -0,0 +1,57 @@
1
+ // Pure helpers voor foutindeling en instellingen van de wachtrijverwerking.
2
+ /** Deelt een foutmelding in. Volgorde is bewust: token vóór sleutel. */
3
+ export function classifyGoogleError(message) {
4
+ const m = (message ?? "").toLowerCase();
5
+ if (!m)
6
+ return "other";
7
+ if (/central auth|token|credential|niet geconfigureerd/.test(m))
8
+ return "token";
9
+ if (/api[_ -]?key|api key not valid|permission_denied|request_denied|ongeldige sleutel|\b403\b|\b401\b/.test(m))
10
+ return "invalid_key";
11
+ if (/quota|resource_exhausted|rate limit|over_query_limit|\b429\b|daglimiet/.test(m))
12
+ return "quota";
13
+ if (/zero_results|not[_ ]found|geen resultaat|niet gevonden|geen route|niet worden omgezet/.test(m))
14
+ return "not_found";
15
+ return "other";
16
+ }
17
+ /** Fouten waarbij doorgaan zinloos is (alleen relevant als de schakelaar aan staat). */
18
+ export function isFatalKeyError(kind) {
19
+ return kind === "invalid_key" || kind === "token";
20
+ }
21
+ export const DEFAULT_MAX_ATTEMPTS = 3;
22
+ export const DEFAULT_DAILY_LIMIT = 2000;
23
+ export const DEFAULT_TIMEZONE = "Europe/Amsterdam";
24
+ /** Ontbrekende waarden vallen terug op het huidige gedrag (alle schakelaars uit). */
25
+ export function resolveProcessorSettings(row) {
26
+ const r = row ?? {};
27
+ const limit = Number(r.daily_call_limit);
28
+ const max = Number(r.max_attempts);
29
+ const maxEnabled = r.max_attempts_enabled === true;
30
+ return {
31
+ dailyCallLimit: Number.isFinite(limit) && r.daily_call_limit != null ? limit : DEFAULT_DAILY_LIMIT,
32
+ maxAttempts: maxEnabled && Number.isFinite(max) && max >= 1 ? Math.round(max) : DEFAULT_MAX_ATTEMPTS,
33
+ claimViaRpc: r.claim_via_rpc_enabled === true,
34
+ perCallLimit: r.per_call_limit_enabled === true,
35
+ stopOnKeyError: r.stop_bij_sleutelfout_enabled === true,
36
+ timezone: typeof r.timezone === "string" && r.timezone ? r.timezone : DEFAULT_TIMEZONE,
37
+ };
38
+ }
39
+ /** Kalenderdatum (yyyy-mm-dd) in de opgegeven tijdzone. */
40
+ export function calendarDay(date, timeZone) {
41
+ try {
42
+ return new Intl.DateTimeFormat("en-CA", {
43
+ timeZone, year: "numeric", month: "2-digit", day: "2-digit",
44
+ }).format(date);
45
+ }
46
+ catch {
47
+ return new Intl.DateTimeFormat("en-CA", {
48
+ timeZone: DEFAULT_TIMEZONE, year: "numeric", month: "2-digit", day: "2-digit",
49
+ }).format(date);
50
+ }
51
+ }
52
+ export class DailyLimitReachedError extends Error {
53
+ constructor() {
54
+ super("Daglimiet bereikt");
55
+ this.name = "DailyLimitReachedError";
56
+ }
57
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=maps-errors.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maps-errors.test.d.ts","sourceRoot":"","sources":["../../src/lib/maps-errors.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { calendarDay, classifyGoogleError, isFatalKeyError, resolveProcessorSettings, } from "./maps-errors";
3
+ describe("classifyGoogleError", () => {
4
+ it("herkent quota", () => expect(classifyGoogleError("RESOURCE_EXHAUSTED: quota exceeded (429)")).toBe("quota"));
5
+ it("herkent ongeldige sleutel", () => expect(classifyGoogleError("API key not valid. Please pass a valid API key.")).toBe("invalid_key"));
6
+ it("herkent adres niet gevonden", () => expect(classifyGoogleError("Geen resultaat van Google")).toBe("not_found"));
7
+ it("herkent tokenfout van Central", () => expect(classifyGoogleError("Central Auth weigert het secret van deze module")).toBe("token"));
8
+ it("onbekend is other", () => expect(classifyGoogleError("iets raars")).toBe("other"));
9
+ it("alleen sleutel- en tokenfouten zijn fataal", () => {
10
+ expect(isFatalKeyError("invalid_key")).toBe(true);
11
+ expect(isFatalKeyError("token")).toBe(true);
12
+ expect(isFatalKeyError("quota")).toBe(false);
13
+ });
14
+ });
15
+ describe("resolveProcessorSettings", () => {
16
+ it("ontbrekende instellingen = huidig gedrag (limiet 2000, 3 pogingen, alles uit)", () => {
17
+ expect(resolveProcessorSettings(null)).toEqual({
18
+ dailyCallLimit: 2000,
19
+ maxAttempts: 3,
20
+ claimViaRpc: false,
21
+ perCallLimit: false,
22
+ stopOnKeyError: false,
23
+ timezone: "Europe/Amsterdam",
24
+ });
25
+ });
26
+ it("max_attempts telt alleen als de schakelaar aan staat", () => {
27
+ expect(resolveProcessorSettings({ max_attempts: 7 }).maxAttempts).toBe(3);
28
+ expect(resolveProcessorSettings({ max_attempts: 7, max_attempts_enabled: true }).maxAttempts).toBe(7);
29
+ });
30
+ it("neemt de daglimiet uit de instellingen", () => {
31
+ expect(resolveProcessorSettings({ daily_call_limit: 50 }).dailyCallLimit).toBe(50);
32
+ });
33
+ });
34
+ describe("calendarDay", () => {
35
+ it("gebruikt de tijdzone voor de daggrens", () => {
36
+ // 23:30 UTC op 9 nov = 00:30 op 10 nov in Amsterdam
37
+ expect(calendarDay(new Date(Date.UTC(2026, 10, 9, 23, 30)), "Europe/Amsterdam")).toBe("2026-11-10");
38
+ expect(calendarDay(new Date(Date.UTC(2026, 10, 9, 23, 30)), "UTC")).toBe("2026-11-09");
39
+ });
40
+ });
@@ -1,3 +1,5 @@
1
+ /** Terugvalwaarde (huidig gedrag) voor het maximum aantal pogingen. */
2
+ export declare const MAX_ATTEMPTS = 3;
1
3
  export interface ProcessResult {
2
4
  processed: number;
3
5
  geocoded: number;
@@ -1 +1 @@
1
- {"version":3,"file":"maps-processor.server.d.ts","sourceRoot":"","sources":["../../src/lib/maps-processor.server.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,+DAA+D;AAC/D,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAS9D;AAcD,0EAA0E;AAC1E,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAUxD;AAED,2CAA2C;AAC3C,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMvD;AAgHD,yEAAyE;AACzE,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,aAAa,CAAC,CAmMpE"}
1
+ {"version":3,"file":"maps-processor.server.d.ts","sourceRoot":"","sources":["../../src/lib/maps-processor.server.ts"],"names":[],"mappings":"AAeA,uEAAuE;AACvE,eAAO,MAAM,YAAY,IAAI,CAAC;AAG9B,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,+DAA+D;AAC/D,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAS9D;AAcD,0EAA0E;AAC1E,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAUxD;AAED,2CAA2C;AAC3C,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMvD;AA2LD,yEAAyE;AACzE,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,aAAa,CAAC,CAyNpE"}
@@ -2,8 +2,10 @@
2
2
  // Leest maps_request_queue, geocodet adressen en haalt reistijden op.
3
3
  import { createClient } from "@supabase/supabase-js";
4
4
  import { computeRouteMatrix, geocodeAddress } from "./google-maps.server";
5
+ import { DailyLimitReachedError, calendarDay, classifyGoogleError, isFatalKeyError, resolveProcessorSettings, } from "./maps-errors";
5
6
  const BATCH_SIZE = 50;
6
- const MAX_ATTEMPTS = 3;
7
+ /** Terugvalwaarde (huidig gedrag) voor het maximum aantal pogingen. */
8
+ export const MAX_ATTEMPTS = 3;
7
9
  const MAX_MATRIX_SIDE = 25;
8
10
  /** SHA-256 hash via Web Crypto (browser, Node 15+ en Deno). */
9
11
  export async function addressHash(raw) {
@@ -69,15 +71,81 @@ async function callsToday(db) {
69
71
  const rows = Array.isArray(data) ? data : [];
70
72
  return rows.reduce((sum, row) => sum + (row.elements ?? 0), 0);
71
73
  }
74
+ const noop = async () => { };
75
+ /**
76
+ * Verhoogt de dagteller atomair in de database (alleen bij per_call_limit_enabled).
77
+ * Gooit DailyLimitReachedError als de limiet bereikt is; dan wordt Google niet aangeroepen.
78
+ */
79
+ function makeConsumeCall(db, cfg, initialUsed) {
80
+ if (!cfg.perCallLimit)
81
+ return noop;
82
+ return async () => {
83
+ const { data, error } = await db.rpc("maps_try_consume_call", {
84
+ p_day: calendarDay(new Date(), cfg.timezone),
85
+ p_limit: cfg.dailyCallLimit,
86
+ p_initial: initialUsed,
87
+ });
88
+ if (error) {
89
+ console.error("[maps] maps_try_consume_call", error);
90
+ throw new Error(`Daglimietteller niet bereikbaar: ${error.message}`);
91
+ }
92
+ if (data == null)
93
+ throw new DailyLimitReachedError();
94
+ };
95
+ }
96
+ async function claimItems(db, cfg) {
97
+ if (cfg.claimViaRpc) {
98
+ const { data, error } = await db.rpc("maps_claim_queue_items", {
99
+ p_limit: BATCH_SIZE,
100
+ p_max_attempts: cfg.maxAttempts,
101
+ });
102
+ if (error) {
103
+ console.error("[maps] maps_claim_queue_items", error);
104
+ throw new Error(`Wachtrij claimen mislukt: ${error.message}`);
105
+ }
106
+ return Array.isArray(data) ? data : [];
107
+ }
108
+ // Huidig gedrag (schakelaar uit): eerst lezen, dan op processing zetten.
109
+ const { data: queue, error } = await db
110
+ .from("maps_request_queue")
111
+ .select("*")
112
+ .eq("status", "pending")
113
+ .lt("attempts", cfg.maxAttempts)
114
+ .order("created_at", { ascending: true })
115
+ .limit(BATCH_SIZE);
116
+ if (error)
117
+ console.error("[maps] wachtrij lezen", error);
118
+ const items = Array.isArray(queue) ? queue : [];
119
+ if (items.length === 0)
120
+ return items;
121
+ const { error: updError } = await db
122
+ .from("maps_request_queue")
123
+ .update({ status: "processing", updated_at: new Date().toISOString() })
124
+ .in("id", items.map((item) => item.id));
125
+ if (updError)
126
+ console.error("[maps] wachtrij claimen", updError);
127
+ return items;
128
+ }
129
+ async function releaseItem(db, id) {
130
+ // Terug naar pending zonder extra poging (limiet of afgebroken batch).
131
+ const { error } = await db
132
+ .from("maps_request_queue")
133
+ .update({ status: "pending", updated_at: new Date().toISOString() })
134
+ .eq("id", id);
135
+ if (error)
136
+ console.error("[maps] item vrijgeven", error);
137
+ }
72
138
  async function logUsage(db, callType, elements, outcome, detail) {
73
- await db.from("maps_usage_log").insert({
139
+ const { error } = await db.from("maps_usage_log").insert({
74
140
  call_type: callType,
75
141
  elements,
76
142
  outcome,
77
143
  detail: detail ?? null,
78
144
  });
145
+ if (error)
146
+ console.error("[maps] usage log", error);
79
147
  }
80
- async function ensureAddress(db, raw) {
148
+ async function ensureAddress(db, raw, beforeCall = noop) {
81
149
  const hash = await addressHash(raw);
82
150
  const { data: existing } = await db
83
151
  .from("maps_addresses")
@@ -97,6 +165,7 @@ async function ensureAddress(db, raw) {
97
165
  .single()).data?.id;
98
166
  if (!id)
99
167
  return null;
168
+ await beforeCall();
100
169
  try {
101
170
  const result = await geocodeAddress(raw);
102
171
  if (!result) {
@@ -141,6 +210,7 @@ async function ensureAddress(db, raw) {
141
210
  export async function processMapsQueueBatch() {
142
211
  const db = adminClient();
143
212
  const settings = await getSettings(db);
213
+ const cfg = resolveProcessorSettings(settings);
144
214
  const result = {
145
215
  processed: 0,
146
216
  geocoded: 0,
@@ -149,36 +219,38 @@ export async function processMapsQueueBatch() {
149
219
  skipped: null,
150
220
  };
151
221
  const used = await callsToday(db);
152
- if (settings && used >= settings.daily_call_limit) {
222
+ if (settings && used >= cfg.dailyCallLimit) {
153
223
  await logUsage(db, "limit", 0, "skipped", "Daglimiet bereikt");
154
224
  result.skipped = "Daglimiet bereikt";
155
225
  return result;
156
226
  }
157
- const { data: queue } = await db
158
- .from("maps_request_queue")
159
- .select("*")
160
- .eq("status", "pending")
161
- .lt("attempts", MAX_ATTEMPTS)
162
- .order("created_at", { ascending: true })
163
- .limit(BATCH_SIZE);
164
- const items = Array.isArray(queue) ? queue : [];
227
+ const items = await claimItems(db, cfg);
165
228
  if (items.length === 0)
166
229
  return result;
167
- const ids = items.map((item) => item.id);
168
- await db
169
- .from("maps_request_queue")
170
- .update({ status: "processing", updated_at: new Date().toISOString() })
171
- .in("id", ids);
230
+ const beforeCall = makeConsumeCall(db, cfg, used);
231
+ let aborted = false;
232
+ let limitLogged = false;
233
+ const onLimit = async () => {
234
+ if (!limitLogged) {
235
+ limitLogged = true;
236
+ await logUsage(db, "limit", 0, "skipped", "Daglimiet bereikt");
237
+ }
238
+ result.skipped = "Daglimiet bereikt";
239
+ };
172
240
  const ttlDays = settings?.cache_ttl_days ?? 30;
173
241
  const expiresAt = new Date(Date.now() + ttlDays * 86400000).toISOString();
174
242
  const pairs = [];
175
243
  for (const item of items) {
244
+ if (aborted) {
245
+ await releaseItem(db, item.id);
246
+ continue;
247
+ }
176
248
  try {
177
249
  if (item.kind === "geocode") {
178
250
  const raw = item.raw_origin ?? item.raw_destination;
179
251
  if (!raw)
180
252
  throw new Error("Geen adres in wachtrijregel");
181
- const address = await ensureAddress(db, raw);
253
+ const address = await ensureAddress(db, raw, beforeCall);
182
254
  if (address)
183
255
  result.geocoded += 1;
184
256
  else
@@ -196,9 +268,9 @@ export async function processMapsQueueBatch() {
196
268
  result.processed += 1;
197
269
  continue;
198
270
  }
199
- const origin = item.raw_origin ? await ensureAddress(db, item.raw_origin) : null;
271
+ const origin = item.raw_origin ? await ensureAddress(db, item.raw_origin, beforeCall) : null;
200
272
  const destination = item.raw_destination
201
- ? await ensureAddress(db, item.raw_destination)
273
+ ? await ensureAddress(db, item.raw_destination, beforeCall)
202
274
  : null;
203
275
  if (!origin || !destination) {
204
276
  result.failed += 1;
@@ -218,12 +290,20 @@ export async function processMapsQueueBatch() {
218
290
  pairs.push({ item, origin, destination });
219
291
  }
220
292
  catch (error) {
293
+ if (error instanceof DailyLimitReachedError) {
294
+ await releaseItem(db, item.id);
295
+ await onLimit();
296
+ aborted = true;
297
+ continue;
298
+ }
221
299
  const message = error instanceof Error ? error.message : String(error);
300
+ if (cfg.stopOnKeyError && isFatalKeyError(classifyGoogleError(message)))
301
+ aborted = true;
222
302
  result.failed += 1;
223
303
  await db
224
304
  .from("maps_request_queue")
225
305
  .update({
226
- status: (item.attempts ?? 0) + 1 >= MAX_ATTEMPTS ? "failed" : "pending",
306
+ status: (item.attempts ?? 0) + 1 >= cfg.maxAttempts ? "failed" : "pending",
227
307
  attempts: (item.attempts ?? 0) + 1,
228
308
  last_error: message,
229
309
  updated_at: new Date().toISOString(),
@@ -241,7 +321,13 @@ export async function processMapsQueueBatch() {
241
321
  const origins = chunk.map((pair) => pair.origin);
242
322
  const destinations = chunk.map((pair) => pair.destination);
243
323
  const mode = chunk[0]?.item.mode ?? "drive";
324
+ if (aborted) {
325
+ for (const pair of chunk)
326
+ await releaseItem(db, pair.item.id);
327
+ continue;
328
+ }
244
329
  try {
330
+ await beforeCall();
245
331
  const elements = await computeRouteMatrix(origins, destinations, mode);
246
332
  await logUsage(db, "matrix", chunk.length, "ok");
247
333
  for (let position = 0; position < chunk.length; position += 1) {
@@ -290,7 +376,16 @@ export async function processMapsQueueBatch() {
290
376
  .eq("id", settings.id);
291
377
  }
292
378
  catch (error) {
379
+ if (error instanceof DailyLimitReachedError) {
380
+ for (const pair of chunk)
381
+ await releaseItem(db, pair.item.id);
382
+ await onLimit();
383
+ aborted = true;
384
+ continue;
385
+ }
293
386
  const message = error instanceof Error ? error.message : String(error);
387
+ if (cfg.stopOnKeyError && isFatalKeyError(classifyGoogleError(message)))
388
+ aborted = true;
294
389
  await logUsage(db, "matrix", chunk.length, "error", message);
295
390
  await db
296
391
  .from("maps_settings")
@@ -302,7 +397,7 @@ export async function processMapsQueueBatch() {
302
397
  await db
303
398
  .from("maps_request_queue")
304
399
  .update({
305
- status: (pair.item.attempts ?? 0) + 1 >= MAX_ATTEMPTS ? "failed" : "pending",
400
+ status: (pair.item.attempts ?? 0) + 1 >= cfg.maxAttempts ? "failed" : "pending",
306
401
  attempts: (pair.item.attempts ?? 0) + 1,
307
402
  last_error: message,
308
403
  updated_at: new Date().toISOString(),
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=maps-processor.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maps-processor.test.d.ts","sourceRoot":"","sources":["../../src/lib/maps-processor.test.ts"],"names":[],"mappings":""}