@odla-ai/chapter 0.56.2 → 0.58.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
@@ -1850,6 +1850,74 @@ These bite silently — a smoke test won't catch them:
1850
1850
  named and whether that person joined without folding the seat status machine
1851
1851
  itself; `NamedSeatRecipientLine` renders it.
1852
1852
 
1853
+ **Availability saves keep what the editor does not show (0.58.0).**
1854
+ `PUT /api/admin/scheduling` now merges the fields it receives onto the
1855
+ stored config before validating, so an editor that sends only hours, days,
1856
+ slot length, timezone, notice and window no longer resets the branded
1857
+ `summaryTemplate` to the generic default. A stored config the rules reject
1858
+ answers `409` on `GET` with the field errors (the next save repairs it), and a
1859
+ failed database write answers `503` with `code: "scheduling_write_failed"`
1860
+ and the database's own code as `cause`, never a bare 500. The default
1861
+ booking window is now 21 days (three weeks of offered times); a group row
1862
+ that already stores `windowDays` keeps it.
1863
+
1864
+ **Decisions after the conversation (0.57.0).** Four operations close the
1865
+ gaps a chapter hits once applicants are real people (BNF bugs fa96b11c,
1866
+ f7bc92ee, 38000934, c3c1d338, 79590edc):
1867
+
1868
+ - `POST /api/admin/applications/:id/decline` refuses an applicant who holds
1869
+ no payment: a free-tier applicant, or a paid-tier applicant who never
1870
+ paid. The application closes as `declined` (`canceled: true`), the CRM
1871
+ stage follows, and nothing is emailed. Under a network authority the
1872
+ answer to "is a payment held?" comes from the authority's access record,
1873
+ not from local provider ids, so a checkout that was started and abandoned
1874
+ can still be declined; a held payment is refused with `payment_held` and
1875
+ the refund is the only way to end it. A gifted seat's recipient is refused
1876
+ with `named_seat_recipient`. `GET …/actions` offers `declined` under an
1877
+ authority only while no checkout has reached the provider, the hub's
1878
+ delegated stage move runs the same operation, and `RecordActions` carries
1879
+ a Decline control beside Approve and Refund (`admin.records.decline`,
1880
+ `admin.records.declined` copy).
1881
+ - `POST /api/admin/applications/:id/refund` with a JSON body
1882
+ `{ continueAs: "<free tier id>" }` is "not a paid fit": the ordinary refund,
1883
+ then the same application moves to that free tier at the last approvable
1884
+ stage and is approved at once, so the person keeps member access as, for
1885
+ example, an Associate. The refunded purchase is archived in
1886
+ `membershipCheckoutHistory`, its billing fields leave the row, and the tier
1887
+ is checked before any money moves. Repeating the call re-approves without a
1888
+ second refund. The response adds `continued: { tierId, status, rolePromoted,
1889
+ emailLogged }`.
1890
+ - The purchaser may give an invited or accepted seat to a different person
1891
+ until it is active: `POST /api/named-seat/regift`
1892
+ `{ recipientName, recipientEmail }` from the members page
1893
+ (`NamedSeatPurchase` shows the control while the offer's `seat.regiftable`
1894
+ is true), or `POST /api/admin/applications/:id/named-seat/regift` by a super
1895
+ admin on the purchaser's behalf (`NamedSeatRecordPanel`). The authority's
1896
+ `seat-regift` command keeps the payment and renewal line exactly as they
1897
+ are, so an included seat works the same way; the previous invitation stops
1898
+ resolving, a new invitation effect is queued, the former recipient is kept
1899
+ on the seat's `formerRecipients`, and a recipient who had already accepted
1900
+ gets a `release_named_recipient` effect that closes their application as
1901
+ `declined` without a refund (the chapter also does this at once). Active
1902
+ seats are final (`seat_not_regiftable`). Upgrade and re-provision the
1903
+ network owner first: the seat gains optional `regiftedAt` and
1904
+ `formerRecipients`, and an effect optional `recipientUserId` and
1905
+ `recipientApplicationId`.
1906
+ - A second-seat invitation goes out only once the buying member is approved.
1907
+ `drainMembershipEffects` holds an `invite_named_recipient` effect while the
1908
+ primary is `paid_pending_approval` and reports it under `held`; the
1909
+ receiver's eligibility read says the same. The offer's `seat` carries
1910
+ `invitationHeld: true` meanwhile, and `NamedSeatRecipientLine` says the
1911
+ invitation is sent once the membership is approved.
1912
+ - Anyone whose earlier applications in the chapter all ended without
1913
+ approval (declined, refunded, canceled) may apply again from the public
1914
+ form. The new attempt is a new application row linked to the same person:
1915
+ it adopts the account bound to the closed attempt, takes over the unique
1916
+ signup key, and re-stages the same CRM person; the closed row stays as
1917
+ history. An open application still answers `account_already_exists`.
1918
+ `resolveSignupEligibility` returns the predecessor it chose, and
1919
+ `closedApplication` is the one definition of "ended without approval".
1920
+
1853
1921
  Delivery logs distinguish sent, logged-only, in-flight, uncertain and
1854
1922
  retry-approved outcomes. The authenticated operator resolution route
1855
1923
  `/api/admin/email/log/:id/resolve` records evidence against the exact attempt;
@@ -2,7 +2,7 @@ import {
2
2
  DEFAULT_JOIN_COPY,
3
3
  formatChapterCopy,
4
4
  useChapterCopy
5
- } from "./chunk-4M2PN5K3.js";
5
+ } from "./chunk-FHKZVF5G.js";
6
6
  import {
7
7
  collectFormFields
8
8
  } from "./chunk-IVXVECKR.js";
@@ -1048,7 +1048,7 @@ var NAMED_SEAT_RECIPIENT_WORDS = {
1048
1048
  };
1049
1049
  var day = (at) => new Date(at).toLocaleDateString("en-US", { timeZone: "UTC" });
1050
1050
  function NamedSeatRecipientLine({ seat }) {
1051
- const words = NAMED_SEAT_RECIPIENT_WORDS[seat.recipientState] ?? seat.status.replaceAll("_", " ");
1051
+ const words = seat.invitationHeld ? "named; the invitation is sent once your membership is approved" : NAMED_SEAT_RECIPIENT_WORDS[seat.recipientState] ?? seat.status.replaceAll("_", " ");
1052
1052
  return /* @__PURE__ */ jsxs13("p", { className: "member-named-seat-recipient", children: [
1053
1053
  seat.recipientName,
1054
1054
  " \xB7 ",
@@ -1062,6 +1062,7 @@ function NamedSeatPurchase({ api }) {
1062
1062
  const [data, setData] = useState10(null), [error, setError] = useState10("");
1063
1063
  const [name, setName] = useState10(""), [email, setEmail] = useState10(""), [reviewing, setReviewing] = useState10(false);
1064
1064
  const [terms, setTerms] = useState10(false), [pending, setPending] = useState10(false), [granting, setGranting] = useState10(false);
1065
+ const [renaming, setRenaming] = useState10(false), [nextName, setNextName] = useState10(""), [nextEmail, setNextEmail] = useState10("");
1065
1066
  const load = async () => {
1066
1067
  try {
1067
1068
  const value = await api("/api/named-seat");
@@ -1098,11 +1099,54 @@ function NamedSeatPurchase({ api }) {
1098
1099
  }
1099
1100
  };
1100
1101
  const money = data ? new Intl.NumberFormat("en-US", { style: "currency", currency: data.currency }).format(data.amountCents / 100) : "";
1102
+ const regift = async (event) => {
1103
+ event.preventDefault();
1104
+ setGranting(true);
1105
+ setError("");
1106
+ try {
1107
+ await api("/api/named-seat/regift", {
1108
+ method: "POST",
1109
+ headers: { "content-type": "application/json" },
1110
+ body: JSON.stringify({ recipientName: nextName, recipientEmail: nextEmail })
1111
+ });
1112
+ setRenaming(false);
1113
+ await load();
1114
+ } catch {
1115
+ setError("The seat could not be given to someone else. Refresh to check its status before trying again.");
1116
+ } finally {
1117
+ setGranting(false);
1118
+ }
1119
+ };
1101
1120
  return /* @__PURE__ */ jsx14(JourneyFrame, { children: /* @__PURE__ */ jsxs13("section", { className: "card member-named-seat", "aria-label": "Named additional seat", children: [
1102
1121
  /* @__PURE__ */ jsx14("h2", { children: "Named additional seat" }),
1103
1122
  error ? /* @__PURE__ */ jsx14("p", { role: "alert", children: error }) : null,
1104
1123
  data ? /* @__PURE__ */ jsxs13(Fragment3, { children: [
1105
1124
  data.seat ? /* @__PURE__ */ jsx14(NamedSeatRecipientLine, { seat: data.seat }) : null,
1125
+ data.seat?.regiftable && !renaming ? /* @__PURE__ */ jsx14("button", { className: "btn secondary", type: "button", onClick: () => {
1126
+ setRenaming(true);
1127
+ setNextName("");
1128
+ setNextEmail("");
1129
+ }, children: "Give the seat to someone else" }) : null,
1130
+ data.seat?.regiftable && renaming ? /* @__PURE__ */ jsxs13("form", { className: "member-named-seat-regift", "aria-label": "Give the seat to someone else", onSubmit: (event) => void regift(event), children: [
1131
+ /* @__PURE__ */ jsxs13("p", { children: [
1132
+ "The seat moves to the person you name here. ",
1133
+ data.seat.recipientName,
1134
+ "'s invitation stops working, and nothing is charged or refunded."
1135
+ ] }),
1136
+ /* @__PURE__ */ jsxs13("label", { children: [
1137
+ "New recipient name",
1138
+ /* @__PURE__ */ jsx14("input", { required: true, maxLength: 160, value: nextName, onInput: (event) => setNextName(event.currentTarget.value) })
1139
+ ] }),
1140
+ /* @__PURE__ */ jsxs13("label", { children: [
1141
+ "New recipient email",
1142
+ /* @__PURE__ */ jsx14("input", { required: true, type: "email", maxLength: 254, value: nextEmail, onInput: (event) => setNextEmail(event.currentTarget.value) })
1143
+ ] }),
1144
+ /* @__PURE__ */ jsx14("button", { className: "btn primary", type: "submit", disabled: granting, children: granting ? "Changing the recipient\u2026" : "Give the seat to this person" }),
1145
+ /* @__PURE__ */ jsxs13("button", { className: "btn secondary", type: "button", disabled: granting, onClick: () => setRenaming(false), children: [
1146
+ "Keep ",
1147
+ data.seat.recipientName
1148
+ ] })
1149
+ ] }) : null,
1106
1150
  data.eligible ? /* @__PURE__ */ jsxs13(Fragment3, { children: [
1107
1151
  /* @__PURE__ */ jsxs13("p", { children: [
1108
1152
  data.included ? "Included with your membership at no charge." : `${money} paid in full now.`,
@@ -2562,4 +2606,4 @@ export {
2562
2606
  membershipApplicationHref,
2563
2607
  MembershipCards
2564
2608
  };
2565
- //# sourceMappingURL=chunk-E7CB5K2D.js.map
2609
+ //# sourceMappingURL=chunk-CKC6X5H2.js.map