@temboplus/afloat 0.2.1-beta.11 → 0.2.1-beta.13

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.
@@ -3,4 +3,5 @@ export * from "./wallet.model.js";
3
3
  export * from "./statement-entry.model.js";
4
4
  export * from "./narration.model.js";
5
5
  export * from "./wallet.repository.js";
6
+ export * from "./wallet.timezone.js";
6
7
  export * from "./wallet.utils.js";
@@ -147,16 +147,27 @@ export declare class WalletRepository extends BaseRepository<typeof contract> {
147
147
  * The statement endpoint filters by calendar day, evaluated against
148
148
  * the EAT-anchored database. Bounds are therefore *plain* date strings
149
149
  * (`YYYY-MM-DD`), not full timestamps — sending a datetime gets
150
- * rejected upstream. If you have `Date` objects from a picker, format
151
- * them with the exported `toPlainDate` helper.
150
+ * rejected upstream.
151
+ *
152
+ * When `timezone` is provided (and not EAT), `range` is interpreted as
153
+ * calendar days in that zone: the SDK pads the upstream EAT request
154
+ * by ±1 day and then post-filters entries whose `valueDate` (an
155
+ * absolute instant, parsed from the API's offset-carrying ISO string)
156
+ * falls outside the caller's window when rendered in `timezone`. The
157
+ * server's own filter is on `valueDate`, so this is consistent.
152
158
  *
153
159
  * @param props - The statement request properties
154
160
  * @param props.wallet - The wallet to get statement for (preferred method)
155
161
  * @param props.accountNo - Alternative: account number to lookup wallet and fetch statement
156
162
  * @param props.range - Required date range. Both bounds are `YYYY-MM-DD`
157
- * strings; they correspond to calendar days in EAT.
163
+ * strings. Without `timezone`, they correspond to calendar days in
164
+ * EAT (unchanged legacy behavior). With `timezone`, they correspond
165
+ * to calendar days in that zone.
158
166
  * @param props.range.startDate - Start of statement period (inclusive), `YYYY-MM-DD`
159
167
  * @param props.range.endDate - End of statement period (inclusive), `YYYY-MM-DD`
168
+ * @param props.timezone - Optional IANA timezone id (e.g.
169
+ * `"America/New_York"`). When omitted or set to `"Africa/Nairobi"`,
170
+ * behavior is byte-identical to before this option existed.
160
171
  * @returns Promise that resolves to an array of validated WalletStatementEntry instances with Narration objects
161
172
  * @throws {Error} If neither wallet nor accountNo is provided
162
173
  * @throws {Error} If accountNo is provided but no matching wallet is found
@@ -178,15 +189,6 @@ export declare class WalletRepository extends BaseRepository<typeof contract> {
178
189
  * range: { startDate: "2024-01-01", endDate: "2024-01-31" },
179
190
  * });
180
191
  *
181
- * // From a Date picker
182
- * const entries = await repo.getStatement({
183
- * wallet,
184
- * range: {
185
- * startDate: toPlainDate(pickedFromDate),
186
- * endDate: toPlainDate(pickedToDate),
187
- * },
188
- * });
189
- *
190
192
  * // Process payout transactions
191
193
  * const payoutEntries = entries.filter(entry => entry.isPayout);
192
194
  * payoutEntries.forEach(entry => {
@@ -201,6 +203,7 @@ export declare class WalletRepository extends BaseRepository<typeof contract> {
201
203
  };
202
204
  wallet?: Wallet;
203
205
  accountNo?: string;
206
+ timezone?: string;
204
207
  }): Promise<WalletStatementEntry[]>;
205
208
  /**
206
209
  * Check if a wallet exists with the given query
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Timezone helpers for the wallet statement flow.
3
+ *
4
+ * The statement endpoint accepts `YYYY-MM-DD` bounds and evaluates them
5
+ * against the EAT-anchored (UTC+3) database. To support a caller who
6
+ * wants "transactions whose `valueDate` falls on these calendar days in
7
+ * timezone `tz`", we:
8
+ *
9
+ * 1. Widen the EAT request by ±1 day, so the upstream window
10
+ * definitely contains every instant the caller cares about. ±1 is
11
+ * mathematically sufficient because the maximum delta from EAT
12
+ * (UTC+3) to any timezone on earth is 15 hours, comfortably under
13
+ * 24h.
14
+ * 2. After the response comes back, filter entries by `valueDate`
15
+ * rendered in the caller's zone, comparing as plain `YYYY-MM-DD`
16
+ * strings (which is correct because string-comparing two ISO
17
+ * calendar dates is equivalent to comparing the dates).
18
+ *
19
+ * The server's filter is on `valueDate` (verified empirically), so the
20
+ * client-side filter uses the same field — no risk of dropping records
21
+ * that were padded in.
22
+ */
23
+ import type { PlainDate } from "./wallet.dtos.js";
24
+ /** IANA id for EAT. The SDK short-circuits all timezone work for this. */
25
+ export declare const EAT_TIMEZONE = "Africa/Nairobi";
26
+ /**
27
+ * Add or subtract whole calendar days from a `YYYY-MM-DD` string.
28
+ * Pure string-in/string-out, no dependency on the JS runtime's local
29
+ * timezone — we pin to UTC noon so DST cliffs and ±1s rounding can't
30
+ * tip the result onto an adjacent day.
31
+ *
32
+ * @example
33
+ * shiftPlainDate("2026-05-15", 1); // → "2026-05-16"
34
+ * shiftPlainDate("2026-05-15", -1); // → "2026-05-14"
35
+ * shiftPlainDate("2026-05-31", 1); // → "2026-06-01" (month wrap)
36
+ * shiftPlainDate("2026-01-01", -1); // → "2025-12-31" (year wrap)
37
+ * shiftPlainDate("2026-03-08", 1); // → "2026-03-09" (DST-safe; US "spring forward" day)
38
+ */
39
+ export declare function shiftPlainDate(date: PlainDate, days: number): PlainDate;
40
+ /**
41
+ * Pad an EAT date range by ±1 day on each side. Used to ensure that an
42
+ * upstream EAT-anchored query returns every transaction that falls on
43
+ * the caller's local-zone window, regardless of timezone offset.
44
+ *
45
+ * @example
46
+ * padEatRange({ startDate: "2026-05-15", endDate: "2026-05-16" });
47
+ * // → { startDate: "2026-05-14", endDate: "2026-05-17" }
48
+ *
49
+ * padEatRange({ startDate: "2026-01-01", endDate: "2026-12-31" });
50
+ * // → { startDate: "2025-12-31", endDate: "2027-01-01" } (year wraps on both ends)
51
+ */
52
+ export declare function padEatRange(range: {
53
+ startDate: PlainDate;
54
+ endDate: PlainDate;
55
+ }): {
56
+ startDate: PlainDate;
57
+ endDate: PlainDate;
58
+ };
59
+ /**
60
+ * Format an absolute instant as a `YYYY-MM-DD` string in the given
61
+ * IANA timezone. Uses `en-CA` locale because that's the one major
62
+ * locale whose default date format is already ISO-like — no need to
63
+ * stitch parts back together by hand.
64
+ *
65
+ * @example
66
+ * // A transaction's valueDate from the API ("+03:00" is EAT)
67
+ * const instant = new Date("2026-05-15T01:30:00+03:00");
68
+ *
69
+ * localDateInZone(instant, "Africa/Nairobi"); // → "2026-05-15"
70
+ * localDateInZone(instant, "UTC"); // → "2026-05-14" (22:30 prev day in UTC)
71
+ * localDateInZone(instant, "America/New_York"); // → "2026-05-14" (18:30 prev day in NY)
72
+ * localDateInZone(instant, "Pacific/Auckland"); // → "2026-05-15" (10:30 same day in NZ)
73
+ */
74
+ export declare function localDateInZone(instant: Date, timezone: string): PlainDate;
75
+ /**
76
+ * True when the caller's selected timezone is effectively EAT. Used to
77
+ * short-circuit padding + post-filtering so existing EAT callers see
78
+ * byte-identical behavior.
79
+ *
80
+ * @example
81
+ * isEatTimezone(undefined); // → true (no zone = legacy EAT behavior)
82
+ * isEatTimezone("Africa/Nairobi"); // → true (canonical EAT id)
83
+ * isEatTimezone("UTC"); // → false
84
+ * isEatTimezone("America/New_York"); // → false
85
+ */
86
+ export declare function isEatTimezone(timezone: string | undefined): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@temboplus/afloat",
3
- "version": "0.2.1-beta.11",
3
+ "version": "0.2.1-beta.13",
4
4
  "description": "A foundational library for Temboplus-Afloat projects.",
5
5
  "main": "./dist/index.cjs.js",
6
6
  "module": "./dist/index.esm.js",