@odla-ai/chapter 0.56.1 → 0.57.0

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.
package/README.md CHANGED
@@ -395,6 +395,45 @@ The same renderer can also replace the placeholder chooser inside
395
395
  ```
396
396
 
397
397
  `initialState` accepts trusted server state. On a browser redirect,
398
+ ### Shared application and additional-member controls
399
+
400
+ `JoinIsland` owns application readiness. Its default Submit button and the
401
+ `disabled` value passed to `renderSubmit` reflect required controls, native
402
+ constraints, host-provided custom validity checks, conditional fields and recipient
403
+ prefill. Use `JoinSubmitButton` in a custom submit slot to retain the packaged
404
+ theme and disabled styling. `submitDisabled` remains an additional host gate.
405
+
406
+ Use `AdditionalMemberFields` on Apply and `payment.additionalMember` for the
407
+ packaged editor. Supply copy, application field names and theme tokens; do not
408
+ reimplement validation, save/error handling, or action spacing in the host.
409
+ Declare the same conditional name/email requirements in the server application
410
+ field contract. The quote remains authoritative for price and saved details.
411
+
412
+ ```tsx
413
+ const memberOptions = {
414
+ copy: { label: "A membership for your mother, daughter, or sister" },
415
+ names: { selected: "familySelected", name: "familyName", email: "familyEmail" },
416
+ };
417
+ const [member, setMember] = useState({ selected: false, name: "", email: "" });
418
+ <JoinIsland config={config} payment={{
419
+ initialNamedSeat: additionalMemberSelection(member) ?? undefined,
420
+ additionalMember: { ...memberOptions, onSaved: setMember },
421
+ namedSeatLabel: "Additional family member",
422
+ }}>
423
+ {/* Other site application fields */}
424
+ <AdditionalMemberFields {...memberOptions} value={member} onChange={setMember} offer={selectedTier?.seatOffer} />
425
+ </JoinIsland>
426
+ ```
427
+
428
+ Import these controls from `@odla-ai/chapter/ui/member`. The editor uses Save
429
+ details for both included and paid members, a separate Cancel action, retryable
430
+ errors, and disabled controls while saving. Checkout remains read-only until
431
+ Edit details is chosen. Legacy `renderNamedSeatEditor` remains available for
432
+ specialized composition; new hosts should use the packaged controls.
433
+ `JourneyFrame` supplies responsive layout and uses `--ui-accent`,
434
+ `--ui-on-accent`, `--ui-text-muted` and `--ui-surface-2` for enabled and disabled
435
+ states. The default checkout editor also uses these shared fields and readiness.
436
+
398
437
  `JoinIsland` reads the `application` capability from the query string and asks
399
438
  `GET /api/join/resume` for the canonical payment, booking, or done state; raw
400
439
  `redirect_status` and `reschedule` values never choose a UI step. Supply
@@ -1811,6 +1850,63 @@ These bite silently — a smoke test won't catch them:
1811
1850
  named and whether that person joined without folding the seat status machine
1812
1851
  itself; `NamedSeatRecipientLine` renders it.
1813
1852
 
1853
+ **Decisions after the conversation (0.57.0).** Four operations close the
1854
+ gaps a chapter hits once applicants are real people (BNF bugs fa96b11c,
1855
+ f7bc92ee, 38000934, c3c1d338, 79590edc):
1856
+
1857
+ - `POST /api/admin/applications/:id/decline` refuses an applicant who holds
1858
+ no payment: a free-tier applicant, or a paid-tier applicant who never
1859
+ paid. The application closes as `declined` (`canceled: true`), the CRM
1860
+ stage follows, and nothing is emailed. Under a network authority the
1861
+ answer to "is a payment held?" comes from the authority's access record,
1862
+ not from local provider ids, so a checkout that was started and abandoned
1863
+ can still be declined; a held payment is refused with `payment_held` and
1864
+ the refund is the only way to end it. A gifted seat's recipient is refused
1865
+ with `named_seat_recipient`. `GET …/actions` offers `declined` under an
1866
+ authority only while no checkout has reached the provider, the hub's
1867
+ delegated stage move runs the same operation, and `RecordActions` carries
1868
+ a Decline control beside Approve and Refund (`admin.records.decline`,
1869
+ `admin.records.declined` copy).
1870
+ - `POST /api/admin/applications/:id/refund` with a JSON body
1871
+ `{ continueAs: "<free tier id>" }` is "not a paid fit": the ordinary refund,
1872
+ then the same application moves to that free tier at the last approvable
1873
+ stage and is approved at once, so the person keeps member access as, for
1874
+ example, an Associate. The refunded purchase is archived in
1875
+ `membershipCheckoutHistory`, its billing fields leave the row, and the tier
1876
+ is checked before any money moves. Repeating the call re-approves without a
1877
+ second refund. The response adds `continued: { tierId, status, rolePromoted,
1878
+ emailLogged }`.
1879
+ - The purchaser may give an invited or accepted seat to a different person
1880
+ until it is active: `POST /api/named-seat/regift`
1881
+ `{ recipientName, recipientEmail }` from the members page
1882
+ (`NamedSeatPurchase` shows the control while the offer's `seat.regiftable`
1883
+ is true), or `POST /api/admin/applications/:id/named-seat/regift` by a super
1884
+ admin on the purchaser's behalf (`NamedSeatRecordPanel`). The authority's
1885
+ `seat-regift` command keeps the payment and renewal line exactly as they
1886
+ are, so an included seat works the same way; the previous invitation stops
1887
+ resolving, a new invitation effect is queued, the former recipient is kept
1888
+ on the seat's `formerRecipients`, and a recipient who had already accepted
1889
+ gets a `release_named_recipient` effect that closes their application as
1890
+ `declined` without a refund (the chapter also does this at once). Active
1891
+ seats are final (`seat_not_regiftable`). Upgrade and re-provision the
1892
+ network owner first: the seat gains optional `regiftedAt` and
1893
+ `formerRecipients`, and an effect optional `recipientUserId` and
1894
+ `recipientApplicationId`.
1895
+ - A second-seat invitation goes out only once the buying member is approved.
1896
+ `drainMembershipEffects` holds an `invite_named_recipient` effect while the
1897
+ primary is `paid_pending_approval` and reports it under `held`; the
1898
+ receiver's eligibility read says the same. The offer's `seat` carries
1899
+ `invitationHeld: true` meanwhile, and `NamedSeatRecipientLine` says the
1900
+ invitation is sent once the membership is approved.
1901
+ - Anyone whose earlier applications in the chapter all ended without
1902
+ approval (declined, refunded, canceled) may apply again from the public
1903
+ form. The new attempt is a new application row linked to the same person:
1904
+ it adopts the account bound to the closed attempt, takes over the unique
1905
+ signup key, and re-stages the same CRM person; the closed row stays as
1906
+ history. An open application still answers `account_already_exists`.
1907
+ `resolveSignupEligibility` returns the predecessor it chose, and
1908
+ `closedApplication` is the one definition of "ended without approval".
1909
+
1814
1910
  Delivery logs distinguish sent, logged-only, in-flight, uncertain and
1815
1911
  retry-approved outcomes. The authenticated operator resolution route
1816
1912
  `/api/admin/email/log/:id/resolve` records evidence against the exact attempt;