@@ -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,105 @@ 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 AFTER the defaults. Note that
170+ * `active` and `activationDate` are already set by this factory;
171+ * overriding them defeats its purpose.
172+ */
173+ extra ?: Record < string , unknown > ;
174+ }
175+
176+ /**
177+ * Create an ACTIVE client owned by the current test and queue its
178+ * deletion on the supplied {@link CleanupGuard}.
179+ *
180+ * Why this exists as a separate factory rather than an
181+ * `{ active: true }` flag on {@link createTestClient}: every flow that
182+ * opens a loan account, opens a savings account, or applies a client
183+ * charge requires an active client, and each of those flows carries a
184+ * date-ordering constraint relative to the activation date. Encoding
185+ * the chain once here keeps ~20 downstream specs from re-deriving it.
186+ *
187+ * Cleanup caveat: Fineract only hard-deletes clients that are pending
188+ * with no attached accounts. The deleter registered here will
189+ * therefore FAIL for an active client, and the {@link CleanupGuard}
190+ * will report it in `summary.failed` without throwing. That is
191+ * deliberate and accepted — E2E databases are ephemeral, and the
192+ * `E2E_` name prefix keeps orphans greppable. The registration is
193+ * kept (rather than skipped) so that a test which happens to leave the
194+ * client account-free still gets it removed.
195+ *
196+ * @param setup The per-test {@link ApiSetupManager}.
197+ * @param guard The per-test {@link CleanupGuard}.
198+ * @param overrides See {@link CreateActiveTestClientOverrides}.
199+ * @returns A {@link TestClient} projection. No follow-up GET is
200+ * issued; callers needing the activation timeline should
201+ * call `setup.api.getClient(id)` themselves.
202+ * @throws When `activationDate` precedes `submittedOnDate`, so the
203+ * failure names the offending fields instead of surfacing as
204+ * an opaque Fineract validation error.
205+ */
206+ export async function createActiveTestClient (
207+ setup : ApiSetupManager ,
208+ guard : CleanupGuard ,
209+ overrides : CreateActiveTestClientOverrides = { }
210+ ) : Promise < TestClient > {
211+ const submittedOnDate = overrides . submittedOnDate ?? DEFAULT_TEST_CLIENT_SUBMITTED_ON_DATE ;
212+ const activationDate = overrides . activationDate ?? DEFAULT_TEST_CLIENT_ACTIVATION_DATE ;
213+
214+ assertDateNotBefore ( activationDate , submittedOnDate , 'createActiveTestClient' , 'activationDate' , 'submittedOnDate' ) ;
215+
216+ return createTestClient ( setup , guard , {
217+ firstname : overrides . firstname ,
218+ lastname : overrides . lastname ,
219+ officeId : overrides . officeId ,
220+ submittedOnDate,
221+ extra : {
222+ active : true ,
223+ activationDate,
224+ ...overrides . extra
225+ }
226+ } ) ;
227+ }
228+
229+ /**
230+ * Guard that `later` is not chronologically before `earlier`.
231+ *
232+ * Both inputs use Fineract's `dd MMMM yyyy` wire format, which
233+ * `Date.parse` handles natively. An unparseable input is treated as a
234+ * programming error rather than silently skipped, because a typo in a
235+ * date literal would otherwise disable the very check that catches
236+ * date-ordering bugs.
237+ */
238+ function assertDateNotBefore (
239+ later : string ,
240+ earlier : string ,
241+ caller : string ,
242+ laterLabel : string ,
243+ earlierLabel : string
244+ ) : void {
245+ const laterMs = Date . parse ( later ) ;
246+ const earlierMs = Date . parse ( earlier ) ;
247+
248+ if ( Number . isNaN ( laterMs ) || Number . isNaN ( earlierMs ) ) {
249+ throw new Error (
250+ `${ caller } : unable to parse dates — ${ laterLabel } ='${ later } ', ${ earlierLabel } ='${ earlier } '. ` +
251+ `Expected Fineract's 'dd MMMM yyyy' format, e.g. '01 January 2024'.`
252+ ) ;
253+ }
254+
255+ if ( laterMs < earlierMs ) {
256+ throw new Error (
257+ `${ caller } : ${ laterLabel } ('${ later } ') must not precede ${ earlierLabel } ('${ earlier } ') — ` +
258+ `Fineract rejects the create with a validation error that names neither field.`
259+ ) ;
260+ }
261+ }
0 commit comments