@temboplus/afloat 0.2.1-beta.10 → 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.
@@ -28,8 +28,14 @@ export declare class QueryBuilder {
28
28
  whereLessThanOrEqual(field: string, value: any): this;
29
29
  whereBetween(field: string, min: any, max: any): this;
30
30
  /**
31
- * Filter by date range with support for Date objects, strings, and null
32
- * Internally converts to createdAt:gte and createdAt:lte filters
31
+ * Filter by date range with support for Date objects, strings, and null.
32
+ * Internally converts to createdAt:gte and createdAt:lte filters.
33
+ *
34
+ * Date objects are normalised to UTC-`Z` ISO format on the wire,
35
+ * regardless of subclass. Some host-app helpers (e.g. `@date-fns/tz`'s
36
+ * `TZDate`) override `.toISOString()` to emit `+HH:MM` offset form;
37
+ * routing through `new Date(d.getTime())` strips the zone identity and
38
+ * forces the canonical UTC representation.
33
39
  */
34
40
  whereDateBetween(startDate?: string | Date | null, endDate?: string | Date | null): this;
35
41
  addSort(criteria: SortCriteria): this;
@@ -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";
@@ -86,16 +86,16 @@ export declare const contract: {
86
86
  summary: "Get Wallet Statement";
87
87
  method: "POST";
88
88
  body: z.ZodObject<{
89
- endDate: z.ZodDate;
90
- startDate: z.ZodDate;
89
+ endDate: z.ZodString;
90
+ startDate: z.ZodString;
91
91
  accountNo: z.ZodOptional<z.ZodString>;
92
92
  }, "strip", z.ZodTypeAny, {
93
- startDate: Date;
94
- endDate: Date;
93
+ startDate: string;
94
+ endDate: string;
95
95
  accountNo?: string | undefined;
96
96
  }, {
97
- startDate: Date;
98
- endDate: Date;
97
+ startDate: string;
98
+ endDate: string;
99
99
  accountNo?: string | undefined;
100
100
  }>;
101
101
  path: "/statement";
@@ -102,6 +102,14 @@ declare const statementEntrySchema: z.ZodObject<{
102
102
  accountNo?: string | undefined;
103
103
  currencyCode?: string | undefined;
104
104
  }>;
105
+ /**
106
+ * Plain calendar date as a `YYYY-MM-DD` string. Used by endpoints that
107
+ * filter against a calendar day rather than a moment in time — e.g. the
108
+ * statement endpoint, which the server evaluates against EAT-anchored
109
+ * data. Validated by regex to avoid silently sending a full ISO
110
+ * timestamp where a date-only string is expected.
111
+ */
112
+ declare const plainDateSchema: z.ZodString;
105
113
  /**
106
114
  * Collection of wallet-related schemas for export.
107
115
  * Provides access to both wallet and statement entry validation schemas.
@@ -197,8 +205,10 @@ export declare const WalletDTOSchemas: {
197
205
  accountNo?: string | undefined;
198
206
  currencyCode?: string | undefined;
199
207
  }>;
208
+ plainDate: z.ZodString;
200
209
  };
201
210
  export type WalletDTO = z.infer<typeof walletSchema>;
202
211
  export type WalletQueryDTO = z.infer<typeof walletQuerySchema>;
203
212
  export type WalletStatementEntryDTO = z.infer<typeof statementEntrySchema>;
213
+ export type PlainDate = z.infer<typeof plainDateSchema>;
204
214
  export {};
@@ -144,55 +144,66 @@ export declare class WalletRepository extends BaseRepository<typeof contract> {
144
144
  * 1. Direct wallet object (preferred for performance and currency context)
145
145
  * 2. Account number lookup (requires additional API call to determine currency)
146
146
  *
147
- * If no date range is provided, defaults to the current month.
148
- * Returns statement entries with enhanced narration objects containing payout detection
149
- * and parsing capabilities.
147
+ * The statement endpoint filters by calendar day, evaluated against
148
+ * the EAT-anchored database. Bounds are therefore *plain* date strings
149
+ * (`YYYY-MM-DD`), not full timestamps — sending a datetime gets
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.
150
158
  *
151
159
  * @param props - The statement request properties
152
160
  * @param props.wallet - The wallet to get statement for (preferred method)
153
161
  * @param props.accountNo - Alternative: account number to lookup wallet and fetch statement
154
- * @param props.range - Optional date range (defaults to current month)
155
- * @param props.range.startDate - Start date for statement period
156
- * @param props.range.endDate - End date for statement period
162
+ * @param props.range - Required date range. Both bounds are `YYYY-MM-DD`
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.
166
+ * @param props.range.startDate - Start of statement period (inclusive), `YYYY-MM-DD`
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.
157
171
  * @returns Promise that resolves to an array of validated WalletStatementEntry instances with Narration objects
158
172
  * @throws {Error} If neither wallet nor accountNo is provided
159
173
  * @throws {Error} If accountNo is provided but no matching wallet is found
174
+ * @throws {Error} If either `range` bound isn't a valid `YYYY-MM-DD` string
160
175
  * @throws {Error} If the statement fetch operation fails or data is invalid
161
176
  *
162
177
  * @example
163
178
  * ```typescript
164
179
  * // Method 1: Using wallet object (recommended)
165
180
  * const wallet = await repo.getWallets().then(w => w[0]);
166
- * const currentMonthEntries = await repo.getStatement({ wallet });
167
- *
168
- * // Method 2: Using account number
169
- * const entriesForAccount = await repo.getStatement({
170
- * accountNo: '123456789'
181
+ * const entries = await repo.getStatement({
182
+ * wallet,
183
+ * range: { startDate: "2024-01-01", endDate: "2024-01-31" },
171
184
  * });
172
185
  *
173
- * // Custom date range with wallet
174
- * const customRangeEntries = await repo.getStatement({
175
- * wallet,
176
- * range: {
177
- * startDate: new Date('2024-01-01'),
178
- * endDate: new Date('2024-01-31')
179
- * }
186
+ * // Method 2: Using account number
187
+ * const byAccount = await repo.getStatement({
188
+ * accountNo: '123456789',
189
+ * range: { startDate: "2024-01-01", endDate: "2024-01-31" },
180
190
  * });
181
191
  *
182
192
  * // Process payout transactions
183
- * const payoutEntries = currentMonthEntries.filter(entry => entry.isPayout);
193
+ * const payoutEntries = entries.filter(entry => entry.isPayout);
184
194
  * payoutEntries.forEach(entry => {
185
195
  * console.log(`Payout ID: ${entry.payoutId}, Amount: ${entry.amountDebited.label}`);
186
196
  * });
187
197
  * ```
188
198
  */
189
199
  getStatement(props: {
190
- range?: {
191
- startDate: Date;
192
- endDate: Date;
200
+ range: {
201
+ startDate: string;
202
+ endDate: string;
193
203
  };
194
204
  wallet?: Wallet;
195
205
  accountNo?: string;
206
+ timezone?: string;
196
207
  }): Promise<WalletStatementEntry[]>;
197
208
  /**
198
209
  * Check if a wallet exists with the given query
@@ -203,3 +214,15 @@ export declare class WalletRepository extends BaseRepository<typeof contract> {
203
214
  */
204
215
  count(query?: WalletQueryInput): Promise<number>;
205
216
  }
217
+ /**
218
+ * Format a `Date` as `YYYY-MM-DD` using its local (browser/runtime)
219
+ * Y/M/D fields. Use this to convert a picker-emitted `Date` into the
220
+ * plain-date string `getStatement` expects.
221
+ *
222
+ * The local-field choice is intentional: a `react-day-picker` (or any
223
+ * grid-based) calendar picks a wall-clock day in the runtime's clock,
224
+ * and that's the calendar day the user expects to filter against. If
225
+ * you need to bias the conversion toward a specific zone, pre-position
226
+ * the `Date` (e.g. via `@date-fns/tz`'s `TZDate`) before calling this.
227
+ */
228
+ export declare function toPlainDate(date: Date): string;
@@ -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.10",
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",