@@ -45,6 +45,28 @@ export const DEFAULT_TEST_CLIENT_LASTNAME = 'E2E';
4545/** Default submitted-on date applied to every pending test client. */
4646export const DEFAULT_TEST_CLIENT_SUBMITTED_ON_DATE = '01 January 2024' ;
4747
48+ /**
49+ * Default activation date for {@link createActiveTestClient}.
50+ *
51+ * Deliberately one day AFTER {@link DEFAULT_TEST_CLIENT_SUBMITTED_ON_DATE}:
52+ * Fineract rejects an activation that predates submission, and the
53+ * resulting error message names neither field, so an accidental
54+ * inversion surfaces as an opaque validation failure.
55+ */
56+ export const DEFAULT_TEST_CLIENT_ACTIVATION_DATE = '02 January 2024' ;
57+
58+ /**
59+ * Earliest date a downstream account or charge may safely use.
60+ *
61+ * Loan applications, savings applications, and client charges are all
62+ * rejected by Fineract when their own date precedes the owning
63+ * client's activation date. Factories in Tracks B/C default their
64+ * `submittedOnDate` to this constant so the whole chain
65+ * (submitted -> activated -> account/charge) is monotonic by
66+ * construction rather than by each spec author remembering the rule.
67+ */
68+ export const DEFAULT_ACCOUNT_OPENING_DATE = '03 January 2024' ;
69+
4870/** Fineract `legalFormId` for an individual person. */
4971const LEGAL_FORM_PERSON = 1 ;
5072
@@ -135,3 +157,117 @@ export async function createTestClient(
135157 officeId
136158 } ;
137159}
160+
161+ /** Caller-supplied tweaks to the default active-client payload. */
162+ export interface CreateActiveTestClientOverrides extends Omit < CreateTestClientOverrides , 'extra' > {
163+ /**
164+ * Override the default activation date. MUST NOT precede
165+ * `submittedOnDate` — Fineract rejects the create outright.
166+ */
167+ activationDate ?: string ;
168+ /**
169+ * Extra payload fields merged BEFORE this factory's own invariants.
170+ *
171+ * `active`, `activationDate`, and `submittedOnDate` are always
172+ * applied last and cannot be overridden here — they are the fields
173+ * this factory validates, and letting `extra` replace them would
174+ * mean validating one value while sending another to Fineract. Use
175+ * the dedicated `submittedOnDate` / `activationDate` options
176+ * instead, which go through {@link assertDateNotBefore}.
177+ */
178+ extra ?: Record < string , unknown > ;
179+ }
180+
181+ /**
182+ * Create an ACTIVE client owned by the current test and queue its
183+ * deletion on the supplied {@link CleanupGuard}.
184+ *
185+ * Why this exists as a separate factory rather than an
186+ * `{ active: true }` flag on {@link createTestClient}: every flow that
187+ * opens a loan account, opens a savings account, or applies a client
188+ * charge requires an active client, and each of those flows carries a
189+ * date-ordering constraint relative to the activation date. Encoding
190+ * the chain once here keeps ~20 downstream specs from re-deriving it.
191+ *
192+ * Cleanup caveat: Fineract only hard-deletes clients that are pending
193+ * with no attached accounts. The deleter registered here will
194+ * therefore FAIL for an active client, and the {@link CleanupGuard}
195+ * will report it in `summary.failed` without throwing. That is
196+ * deliberate and accepted — E2E databases are ephemeral, and the
197+ * `E2E_` name prefix keeps orphans greppable. The registration is
198+ * kept (rather than skipped) so that a test which happens to leave the
199+ * client account-free still gets it removed.
200+ *
201+ * @param setup The per-test {@link ApiSetupManager}.
202+ * @param guard The per-test {@link CleanupGuard}.
203+ * @param overrides See {@link CreateActiveTestClientOverrides}.
204+ * @returns A {@link TestClient} projection. No follow-up GET is
205+ * issued; callers needing the activation timeline should
206+ * call `setup.api.getClient(id)` themselves.
207+ * @throws When `activationDate` precedes `submittedOnDate`, so the
208+ * failure names the offending fields instead of surfacing as
209+ * an opaque Fineract validation error.
210+ */
211+ export async function createActiveTestClient (
212+ setup : ApiSetupManager ,
213+ guard : CleanupGuard ,
214+ overrides : CreateActiveTestClientOverrides = { }
215+ ) : Promise < TestClient > {
216+ const submittedOnDate = overrides . submittedOnDate ?? DEFAULT_TEST_CLIENT_SUBMITTED_ON_DATE ;
217+ const activationDate = overrides . activationDate ?? DEFAULT_TEST_CLIENT_ACTIVATION_DATE ;
218+
219+ assertDateNotBefore ( activationDate , submittedOnDate , 'createActiveTestClient' , 'activationDate' , 'submittedOnDate' ) ;
220+
221+ return createTestClient ( setup , guard , {
222+ firstname : overrides . firstname ,
223+ lastname : overrides . lastname ,
224+ officeId : overrides . officeId ,
225+ submittedOnDate,
226+ extra : {
227+ // Caller extras first, invariants last. `createTestClient`
228+ // spreads this object after its own defaults, so anything left
229+ // in here wins — including `submittedOnDate`. Ordering the
230+ // spread this way keeps the three validated fields
231+ // authoritative: otherwise the guard above could pass on one
232+ // date pair while Fineract received another.
233+ ...overrides . extra ,
234+ active : true ,
235+ activationDate,
236+ submittedOnDate
237+ }
238+ } ) ;
239+ }
240+
241+ /**
242+ * Guard that `later` is not chronologically before `earlier`.
243+ *
244+ * Both inputs use Fineract's `dd MMMM yyyy` wire format, which
245+ * `Date.parse` handles natively. An unparseable input is treated as a
246+ * programming error rather than silently skipped, because a typo in a
247+ * date literal would otherwise disable the very check that catches
248+ * date-ordering bugs.
249+ */
250+ function assertDateNotBefore (
251+ later : string ,
252+ earlier : string ,
253+ caller : string ,
254+ laterLabel : string ,
255+ earlierLabel : string
256+ ) : void {
257+ const laterMs = Date . parse ( later ) ;
258+ const earlierMs = Date . parse ( earlier ) ;
259+
260+ if ( Number . isNaN ( laterMs ) || Number . isNaN ( earlierMs ) ) {
261+ throw new Error (
262+ `${ caller } : unable to parse dates — ${ laterLabel } ='${ later } ', ${ earlierLabel } ='${ earlier } '. ` +
263+ `Expected Fineract's 'dd MMMM yyyy' format, e.g. '01 January 2024'.`
264+ ) ;
265+ }
266+
267+ if ( laterMs < earlierMs ) {
268+ throw new Error (
269+ `${ caller } : ${ laterLabel } ('${ later } ') must not precede ${ earlierLabel } ('${ earlier } ') — ` +
270+ `Fineract rejects the create with a validation error that names neither field.`
271+ ) ;
272+ }
273+ }
0 commit comments