diff --git a/.forge/features/admin-member-dashboard/spec.md b/.forge/features/admin-member-dashboard/spec.md
index 05b20e003..96d4f19b6 100644
--- a/.forge/features/admin-member-dashboard/spec.md
+++ b/.forge/features/admin-member-dashboard/spec.md
@@ -118,14 +118,14 @@ manage current dues status, and remove invalid member accounts.
- `Edit member access`: `EDIT_MEMBERS` or officer access.
- `Effective permissions`: The union of permission bits granted by all roles
linked to the signed-in user.
-- `Current dues status`: Whether an active dues record currently counts for the
- member under the academic-school-year rules.
+- `Current dues status`: Whether the member has an active entitlement for the
+ current academic school year.
- `Payment availability`: Whether unpaid members may start a new Stripe dues
payment. It does not change any member's paid status or history.
-- `Revoke dues`: Mark every active row that would keep the member effectively
- paid as inactive while preserving it for future history.
-- `Mass invalidate dues`: Mark the dues records covered by the officer action
- inactive until every affected member is unpaid, while preserving them.
+- `Revoke dues`: Deactivate the member's current academic-year entitlement while
+ preserving recorded payment history.
+- `Mass invalidate dues`: Deactivate every active current academic-year
+ entitlement while preserving recorded payment history.
## Acceptance criteria
@@ -136,9 +136,8 @@ manage current dues status, and remove invalid member accounts.
- An editor can update a selected member and sees the updated row/detail state.
- An editor can delete a selected member after confirmation, and the deleted
account no longer appears in results.
-- An editor can mark an unpaid member paid for the payable academic school year
- and can click the status to revoke every record that would keep the member
- paid without deleting history.
+- An editor can grant or revoke the selected member's current academic-year
+ entitlement without creating, rewriting, or deleting payment history.
- An editor can pause or enable member-initiated payments; a read-only member
admin can see but cannot change that state.
- Search covers name, email, Discord username, and company.
diff --git a/.forge/features/admin-member-dashboard/srd.md b/.forge/features/admin-member-dashboard/srd.md
index 9eec3e4d2..f94713947 100644
--- a/.forge/features/admin-member-dashboard/srd.md
+++ b/.forge/features/admin-member-dashboard/srd.md
@@ -70,23 +70,21 @@ reusable inputs live in `@forge/validators`.
ID plus member profile values, updates the selected member rather than the
caller, and keeps the code-owned signup response consistent.
- `memberAdmin.deleteAdminMember`: requires edit-member access and deletes the
- selected Member row, signup response, dues rows, and unreferenced member
- uploads. It retains the User, roles, permissions, sessions, and Hacker data.
-- `memberAdmin.setAdminDuesStatus`: requires edit-member access. Marking paid creates
- a manual active record for the payable year with the configured dues amount
- and no Stripe intent, or reactivates an existing payable-year record without
- rewriting its payment metadata. Revoking repeatedly resolves effective rows
- and marks them inactive until the member is unpaid; it does not delete
- history.
+ selected Member row, signup response, dues payment and entitlement rows, and
+ unreferenced member uploads. It retains the User, roles, permissions,
+ sessions, and Hacker data.
+- `memberAdmin.setAdminDuesStatus`: requires edit-member access. Marking paid
+ creates or reactivates the current academic-year entitlement without creating
+ or rewriting a payment. Revoking deactivates that entitlement; neither action
+ changes recorded payment history.
- `memberAdmin.getDuesPaymentConfiguration`: requires read-member access and
returns the persisted global payment-availability setting.
- `memberAdmin.setDuesPaymentsEnabled`: requires edit-member access, upserts the
singleton setting transactionally, and records the before/after value in the
durable admin audit log.
- `memberAdmin.invalidateEffectiveDues`: requires officer access and, in one
- transaction, repeatedly resolves and invalidates rows until every affected
- member is unpaid. The returned count is distinct affected members rather than
- modified rows.
+ transaction, deactivates every active current academic-year entitlement. The
+ returned count is distinct affected members.
- `memberAdmin.exportAdminMembers`: requires read-member access and returns an
escaped CSV for every result matching the current filters and fuzzy search.
- Admin profile-picture and resume procedures resolve the target User from the
@@ -111,17 +109,17 @@ reusable inputs live in `@forge/validators`.
## Data / migration / compatibility
- Existing `Roles`, `Permissions`, `Member`, `FormResponse`, `DuesPayment`,
- `Event`, `EventAttendee`, `Company`, `Employment`, and Discord archive rows
- remain authoritative for their existing domains.
+ `DuesEntitlement`, `Event`, `EventAttendee`, `Company`, `Employment`, and
+ Discord archive rows remain authoritative for their existing domains.
- Add one `DuesConfiguration` singleton row keyed by `global`. It defaults to
`paymentsEnabled = false`; a missing row also reads as paused.
-- Manual dues records use cents, the same configured price, academic-year
- calculation, unique member/year rule, and active/stale semantics as Stripe
- records.
+- Payment rows are immutable recorded history. New rows come from Stripe while
+ legacy manual rows remain preserved. Admin grants and revocations write only
+ the member/year entitlement and never synthesize a payment.
- Legacy Blade dues rows used the UTC calendar year instead of the academic-year
- start and did not reliably distinguish manual grants from Stripe payments.
- Active current-calendar-year rows remain valid compatibility inputs for
- status, individual revoke, filtering, CSV, and mass invalidation.
+ start. The entitlement migration normalizes January-July legacy years and
+ backfills the corresponding entitlement before removing payment activity
+ state.
- Current production `main` continues to interpret the same permission
bitstrings and tables. This feature adds no permission indices and therefore
remains compatible with existing roles and the bootstrap-superadmin script.
diff --git a/.forge/features/admin-member-dashboard/status.md b/.forge/features/admin-member-dashboard/status.md
index 4c73d0439..7baa1ad05 100644
--- a/.forge/features/admin-member-dashboard/status.md
+++ b/.forge/features/admin-member-dashboard/status.md
@@ -6,6 +6,11 @@ Current phase: Complete
## Decision log
+- 2026-08-12: Issue #504 exposed that payment rows were serving as both recorded
+ history and mutable membership state. Payments are now immutable;
+ a separate member/year entitlement owns paid status. Admin grants,
+ revocations, and mass invalidation change entitlements only, and member
+ checkout always targets the current academic year.
- 2026-08-11: Human requested a persisted admin toggle on `/admin/members` to
replace the hardcoded dues-payment cutoff. Existing edit-member access owns
the mutation; readers can see the operational state without changing it.
@@ -18,7 +23,7 @@ Current phase: Complete
backed by page and tRPC authorization checks.
- 2026-06-27: The legacy production dashboard is reference material only. The
Reforge dashboard must use the current member and dues models, including
- academic-year payment history and stale records.
+ academic-year payment history and inactive entitlements.
- 2026-06-27: Human selected member editing, deletion, individual dues
controls, first-class filtering, search, and page sizes 25/50/100/250/500;
payment history is deferred. Detailed behavior still requires reverse-prompt
@@ -106,6 +111,7 @@ Current phase: Complete
- [x] Complete targeted and repository validation.
- [x] Add the persisted member-payment availability control and API contract.
- [x] Validate the payment-availability follow-up and review the admin UI.
+- [x] Separate immutable dues payments from current-year entitlements.
## Validation / commands
@@ -181,9 +187,13 @@ Current phase: Complete
- 2026-08-11 payment-availability follow-up: API and Blade test suites passed;
the 6-scenario dues Playwright run passed; desktop and 390px mobile visual
review confirmed the header control remains compact and overflow-free.
+- 2026-08-12 dues entitlement redesign: admin grant/revoke now changes only the
+ current-year entitlement, bulk invalidation leaves payment history and other
+ years unchanged, and the two focused admin Playwright regressions passed
+ against a disposable PostgreSQL database.
## Links
- PRs:
-- Issues:
+- Issues: #504
- Discord/thread context:
diff --git a/.forge/features/admin-member-dashboard/test-cases.md b/.forge/features/admin-member-dashboard/test-cases.md
index 63283b49d..1e1fb1ae4 100644
--- a/.forge/features/admin-member-dashboard/test-cases.md
+++ b/.forge/features/admin-member-dashboard/test-cases.md
@@ -108,18 +108,20 @@ Expected observations:
Setup:
-- An editor views one unpaid and one paid member.
+- An editor views a member with active dues for the current academic year.
Action:
-- Click the unpaid status to grant dues, then click the paid status to revoke
- dues.
+- Revoke the member's dues, then grant them again.
Expected observations:
-- Manual paid status uses the payable academic year and configured amount.
-- Revoke marks every row that would keep the member paid inactive without
- deleting history.
+- Granting dues reactivates the same current-year entitlement without creating
+ a payment.
+- Revoking deactivates only the current-year entitlement.
+- Existing payment amount, date, Stripe PaymentIntent ID, and row count remain
+ unchanged.
+- No next-academic-year entitlement or payment is created.
- The dashboard updates the status and no payment history UI appears.
### TC-007: Editor deletes a selected member
@@ -162,9 +164,8 @@ Expected observations:
Setup:
-- An officer views members with effective, stale, historical, unrelated active,
- and legacy calendar-year manual dues rows. At least one member has both a
- current academic-year row and a manual calendar-year grant.
+- An officer views members with active current-year, inactive current-year,
+ historical, and future entitlements plus retained payment history.
Action:
@@ -173,9 +174,9 @@ Action:
Expected observations:
- Paste is blocked in typed stages.
-- Rows are resolved and invalidated until every affected member is unpaid,
- including legacy manual grants, in one transaction.
-- Historical and unrelated active rows remain unchanged.
+- Every active current-year entitlement is deactivated in one transaction.
+- Historical and future entitlements remain unchanged.
+- Payment history remains unchanged.
- The UI reports the affected-member count.
### TC-010: Editor manages target-owned files
@@ -246,7 +247,7 @@ Setup:
- A Member has event check-ins by a known operator, archived human Discord
messages across channels and dates, employment rows, Guild profile data,
- linked roles, and retained dues credits.
+ linked roles, and retained dues payment and entitlement rows.
Action:
@@ -363,8 +364,8 @@ Expected observations:
Setup:
-- A selected member was removed, or a current/payable dues record already
- exists when a manual paid action races another write.
+- A selected member was removed, or their current-year entitlement changes
+ while a manual dues action races another write.
Action:
diff --git a/.forge/features/club-analytics/spec.md b/.forge/features/club-analytics/spec.md
index d1fadc769..192a36795 100644
--- a/.forge/features/club-analytics/spec.md
+++ b/.forge/features/club-analytics/spec.md
@@ -189,12 +189,12 @@ separate user-account count.
counts, paid coverage rate, and the change from the comparable point in the
previous academic year.
- Dues history shows:
- - recorded dues credits by week and month;
+ - recorded dues entitlements by week and month;
- cumulative collection curves by academic school year;
- - academic-year payment-count comparisons;
+ - academic-year entitlement-count comparisons;
- progress toward 25%, 50%, 75%, and 90% profile coverage;
- the date each reached milestone was met;
- - active and stale retained dues credits;
+ - active and inactive retained dues entitlements;
- attendance reach and repeat attendance for paid and unpaid profiles.
- Historical academic-year coverage uses retained profiles that existed by the
end of that academic year. The report identifies that denominator and does
@@ -206,9 +206,9 @@ separate user-account count.
points. It contains no edit controls or contact fields.
- A user with separate member-administration access may follow a link to the
existing Members dashboard. Analytics access alone never grants edits.
-- Copy uses `Paid`, `Unpaid`, or `No active dues credit recorded`. It does not
- call a member delinquent or overdue because Blade has no dues deadline or
- waiver policy.
+- Copy uses `Paid`, `Unpaid`, or `No active dues credit recorded`. The
+ user-facing credit maps to `DuesEntitlement`; it does not call a member
+ delinquent or overdue because Blade has no dues deadline or waiver policy.
### Reports and exports
@@ -315,9 +315,10 @@ separate user-account count.
90 days available to return before the report's observation end.
- `Representation gap`: A segment's share of attendees minus its share of all
Member profiles.
-- `Current dues status`: Whether an active dues credit counts for the Member
+- `Current dues status`: Whether an active dues entitlement counts for the Member
profile under the shared academic-school-year rules.
-- `Dues credit`: A retained dues row that may be active or stale.
+- `Dues entitlement`: A retained member/year row whose active state determines
+ whether the member is current for that academic year.
- `Data coverage`: Profiles with a usable value divided by the applicable
profile population.
- `Internal CSV`: A full read-only operational export for a user with
@@ -357,9 +358,9 @@ separate user-account count.
- Discord matched-Member rows expose only the approved analytical identity and
activity counts; full Member presentation opens only under the existing
Member-admin read policy.
-- Dues reporting shows current paid/unpaid counts, payment timing, academic-year
- comparisons, collection pace, milestones, and active/stale history without
- showing dollars.
+- Dues reporting shows current paid/unpaid counts, entitlement timing,
+ academic-year comparisons, collection pace, milestones, and active/inactive
+ history without showing dollars.
- The unpaid-member table contains names and analytical context but no contact
fields or edits.
- Internal CSVs match the active filters and include the full values and named
diff --git a/.forge/features/club-analytics/srd.md b/.forge/features/club-analytics/srd.md
index 62bb740ad..3bb6b69db 100644
--- a/.forge/features/club-analytics/srd.md
+++ b/.forge/features/club-analytics/srd.md
@@ -91,7 +91,8 @@ Additional rules:
2. Blade parses URL search parameters through shared validators and calls
`analytics.getReport` with one normalized filter object.
3. The API repeats access enforcement and selects only needed fields from
- retained Member, non-hackathon Event, EventAttendee, DuesPayment,
+ retained Member, non-hackathon Event, EventAttendee, DuesEntitlement,
+ DuesPayment,
EventFeedbackConfig, and linked FormResponse records.
4. One pure report builder resolves periods, deduplicates attendance, normalizes
feedback metrics, and builds all five section DTOs from the scoped records.
@@ -114,8 +115,9 @@ population metadata are returned with every report.
- Attendance uses `EventAttendee.memberId` and `eventId`. Multiple rows for the
same pair collapse to one distinct attendance. `checkedInAt` is metadata; the
event's scheduled start selects the reporting period.
-- Dues status reuses `buildDuesStatus` so active, stale, payable-year, and
- legacy calendar-year compatibility match existing Member surfaces.
+- Current dues status reads active `DuesEntitlement` rows for the selected
+ academic year. A linked `DuesPayment`, when present, supplies immutable
+ payment metadata without owning membership state.
- Event feedback uses the explicit `EventFeedbackConfig` -> `FormResponse`
association and the core `overall`, `fun`, `learning`, and `discovery`
answers. Invalid stored values are excluded from that metric and reflected in
@@ -257,25 +259,25 @@ are non-negative integers.
### Dues definitions
-- Current paid/unpaid status is calculated for every current Member profile by
- passing all of that member's retained dues rows through `buildDuesStatus` at
- the report reference date.
+- Current paid/unpaid status is calculated from each Member's active
+ `DuesEntitlement` for the report reference date's academic year.
- Current paid coverage is paid profiles divided by all current retained
profiles. Every current profile is expected to pay; no waiver population is
inferred.
-- Recorded-credit trends deduplicate `(memberId, year)` and group by
- `paymentDate`. Active and stale counts remain separate.
-- Academic-year curves use the row's stored dues year and elapsed days from
- August 1. They report distinct recorded credits, active credits, stale
- credits, and recorded-credit coverage.
+- Entitlement trends are already unique by `(memberId, year)` and group by the
+ linked payment date when present, otherwise the entitlement creation date.
+ Active and inactive counts remain separate.
+- Academic-year curves use the entitlement's stored year and elapsed days from
+ August 1. They report distinct recorded entitlements, active entitlements,
+ inactive entitlements, and entitlement coverage.
- A historical academic-year denominator contains retained profiles whose
`dateCreated` is before that academic year's exclusive August 1 end. It is
labeled `retained profiles by year end`.
-- Coverage milestones at 25%, 50%, 75%, and 90% use the first payment date on
- which recorded-credit coverage met the threshold. An unreached milestone has
- a null date.
-- Previous-year pace compares recorded credits at the same elapsed day of the
- academic year. It does not compare dollars.
+- Coverage milestones at 25%, 50%, 75%, and 90% use the first recorded date on
+ which entitlement coverage met the threshold. An unreached milestone has a
+ null date.
+- Previous-year pace compares recorded entitlements at the same elapsed day of
+ the academic year. It does not compare dollars.
- Paid/unpaid engagement uses current dues status crossed with selected-period
distinct attendance, reach, and repeat attendance.
- Named unpaid rows contain Member UUID, display name, graduation year,
diff --git a/.forge/features/club-analytics/status.md b/.forge/features/club-analytics/status.md
index 729eac077..ee0c07ed3 100644
--- a/.forge/features/club-analytics/status.md
+++ b/.forge/features/club-analytics/status.md
@@ -6,6 +6,9 @@ Current phase: Complete
## Decision log
+- 2026-08-12: Issue #504 replaced payment-row activity with one active/inactive
+ `DuesEntitlement` per member and academic year. Club analytics now reads
+ those entitlements; its user-facing "dues credit" copy remains unchanged.
- 2026-07-16: Created the feature bundle on `reforge/club-analytics`.
- 2026-07-16: The user selected non-hackathon Club analytics and dues
reporting; hackathon analytics remains deferred.
diff --git a/.forge/features/club-analytics/test-cases.md b/.forge/features/club-analytics/test-cases.md
index 2f2aed2ca..fc6e9308e 100644
--- a/.forge/features/club-analytics/test-cases.md
+++ b/.forge/features/club-analytics/test-cases.md
@@ -335,13 +335,13 @@ Expected observations:
- Changing demographic or event filters recomputes the rows without exposing
contact or edit data.
-### TC-016: Current dues status reuses effective dues semantics
+### TC-016: Current dues status uses current-year entitlements
Setup:
-- Retain Members with an active current academic-year credit, an inactive
- current-year credit, an active payable-next-year credit after stale rollover,
- an active compatible legacy calendar-year credit, and no dues rows.
+- Retain Members with an active current academic-year entitlement, an inactive
+ current-year entitlement, an active future-year entitlement, an active
+ prior-year entitlement, and no entitlement rows.
Action:
@@ -349,7 +349,9 @@ Action:
Expected observations:
-- Each Member's state matches `buildDuesStatus` for the same rows and date.
+- Only an active entitlement for the reference date's academic year is paid.
+- Payment history and inactive, prior-year, or future-year entitlements do not
+ make a Member current.
- Paid plus unpaid equals every current retained Member profile.
- Current coverage uses that full denominator and contains no dollar/provider
fields.
@@ -359,7 +361,7 @@ Expected observations:
Setup:
- Retain Members created before and after the exclusive end of two academic
- years. Include unique active/stale dues credits on several payment dates.
+ years. Include unique active/inactive entitlements on several recorded dates.
Action:
@@ -369,11 +371,13 @@ Expected observations:
- Each denominator includes only currently retained profiles created before
that year's end and is labeled accordingly.
-- Curves deduplicate member/year, separate active/stale, and order elapsed-day
- points chronologically.
+- Curves preserve one member/year entitlement, separate active/inactive, and
+ order elapsed-day points chronologically.
+- A payment-linked entitlement uses its payment date for chronology; a manual
+ entitlement with no source payment uses its entitlement creation timestamp.
- Previous-year pace compares the same elapsed academic-year day.
-- Each reached 25/50/75/90-percent milestone uses the first qualifying payment
- date; unreached milestones are null.
+- Each reached 25/50/75/90-percent milestone uses the first qualifying
+ entitlement chronology timestamp; unreached milestones are null.
### TC-018: Paid and unpaid engagement uses selected attendance
@@ -572,7 +576,7 @@ Setup:
- Supply current and comparison event types with measured growth and decline,
at least three events in two weekday/time windows, a mature 30-day return
cohort, audience segments on both sides of the Member-profile distribution,
- current and prior-year dues credits, attended unpaid profiles, and no linked
+ current and prior-year dues entitlements, attended unpaid profiles, and no linked
feedback responses.
Action:
@@ -626,7 +630,7 @@ Expected observations:
Setup:
- Supply test fixtures representing an attendance to an unselected event, a
- dues row for an absent Member, a feedback response with malformed JSON
+ dues entitlement for an absent Member, a feedback response with malformed JSON
values, and an unlinked feedback form.
Action:
diff --git a/.forge/features/member-dues-payment/spec.md b/.forge/features/member-dues-payment/spec.md
index 921323e91..3b1c060cf 100644
--- a/.forge/features/member-dues-payment/spec.md
+++ b/.forge/features/member-dues-payment/spec.md
@@ -71,19 +71,19 @@ dues payment history instead of treating each year's rollover as deleted data.
- Preserve dues history for future alumni/admin surfaces.
- Prevent members from paying twice for the same active academic school year.
- Keep the first member-facing payment flow scoped to completed member profiles.
-- Treat the active/stale state of a dues payment as something future admins can
- control, while this member-facing slice only reads and respects that state.
+- Keep financial payments as immutable history and determine current membership
+ from a separate academic-year entitlement. Admin grant and revoke controls
+ are specified in `.forge/features/admin-member-dashboard/spec.md`.
- Store new member dues amounts in cents so Stripe payments and future manual
inserts use the same amount semantics.
-- If a stale record already occupies the current academic school year, let the
- member pay for the next academic school year instead of deleting or
- overwriting that historical record.
+- Let an unpaid member pay for the current academic school year even when an
+ earlier payment for that year remains in history.
### Out of scope
- Member-visible dues history.
- Admin member management beyond the payment-availability control.
-- Admin manual dues payment, comp, revoke, or rollover controls.
+- Admin manual dues grant, revoke, or rollover controls.
- Refund handling.
- Coupon codes or discounts.
- Admin-configurable pricing.
@@ -100,10 +100,10 @@ dues payment history instead of treating each year's rollover as deleted data.
July 31, labeled by start and end year, such as `2026-2027`.
- `Current dues status`: Whether the member is paid, unpaid, or processing for
the current academic school year.
-- `Active dues record`: A dues payment that currently counts toward paid member
- status.
-- `Stale dues record`: A dues payment that remains in history but no longer
- counts toward current paid member status.
+- `Dues entitlement`: The current paid/unpaid membership state for one member
+ and academic school year. It may reference the payment that granted it.
+- `Inactive entitlement`: A revoked academic-year membership state. It does not
+ alter or delete the associated payment.
- `Historical dues record`: A past payment record retained for future admin or
alumni use, even when it no longer grants current dues status.
- `Late-year warning`: The May 31 through July 31 warning that paying now only
@@ -135,6 +135,11 @@ dues payment history instead of treating each year's rollover as deleted data.
- Between May 31 and July 31, `/member/dues` shows the late-year warning before
the member pays.
- Old dues records are not deleted as part of this member-facing payment flow.
+- Payment history alone never grants membership without an active entitlement
+ for the current academic school year.
+- After entitlement revocation, a member can make one new current-year payment;
+ prior payment history remains unchanged and the new payment activates the
+ current-year entitlement.
- When an admin pauses member payments, unpaid members see that dues payments
are paused until further notice and cannot start checkout.
- When an admin enables member payments, the existing dues payment flow becomes
diff --git a/.forge/features/member-dues-payment/srd.md b/.forge/features/member-dues-payment/srd.md
index 244df0ba4..2df9174d3 100644
--- a/.forge/features/member-dues-payment/srd.md
+++ b/.forge/features/member-dues-payment/srd.md
@@ -23,7 +23,7 @@ history for future admin/alumni surfaces.
components.
- `docs/agentic-development/forge-engineering-principles.md#configurability-principles`:
price stays code-configured for the first slice, while year labeling is
- derived from date and active/stale rollover is future admin-controlled.
+ derived from the current UTC date.
- `apps/blade/DESIGN_SYSTEM.md`: dues UI must use the current Blade raised panel
and darker inset surface hierarchy.
@@ -60,10 +60,14 @@ history for future admin/alumni surfaces.
`dues.createPaymentIntent` from the client if server-side setup fails.
4. After Stripe reports a succeeded PaymentIntent, the client calls
`dues.confirmPayment`. The API retrieves the PaymentIntent from Stripe and
- records the dues row idempotently before the UI shows success and redirects.
+ records the immutable payment and activates the matching current-year
+ entitlement in one transaction before the UI shows success and redirects.
5. `/api/membership` handles Stripe webhook `payment_intent.succeeded` events and
calls the same shared idempotent recording helper used by `confirmPayment`.
-6. `@forge/db` owns dues payment history and the singleton dues configuration.
+6. `@forge/db` owns immutable dues payment history, yearly dues entitlements,
+ the current checkout reservation for each member/year, and the singleton
+ dues configuration. The reservation bridges PaymentIntent creation and the
+ later creation of immutable payment history.
7. `@forge/validators` owns pure dues helper functions and output/input schemas
that are shared by API, Blade, and tests.
@@ -73,8 +77,8 @@ history for future admin/alumni surfaces.
- `dues.getStatus`
- Access: protected member.
- Input: none.
- - Output: current/base academic year, payable year, formatted labels,
- `paid`, `paymentYear`, `paidAt`, `amount`, `lateYearWarning`, and the
+ - Output: current academic year, formatted labels, `paid`, `paymentYear`,
+ `paidAt`, `amount`, `lateYearWarning`, and the
admin-controlled payment-availability state.
- Error: `NOT_FOUND` when the authenticated user has no `Member` row.
- `dues.createPaymentIntent`
@@ -82,15 +86,22 @@ history for future admin/alumni surfaces.
- Input: none.
- Errors:
- `NOT_FOUND` when no member exists.
- - `CONFLICT` when an active dues row already counts the member as paid for
- the relevant academic year.
+ - `CONFLICT` when an active current-year entitlement already makes the
+ member paid.
- `PRECONDITION_FAILED` while admins have payments paused.
- `INTERNAL_SERVER_ERROR` when Stripe does not return a client secret.
- Stripe metadata must include `member_id`, `user_id`, and
`academic_year_start`.
+ - Serializes setup per member/current-year, reuses the reserved non-canceled
+ PaymentIntent, and persists the reservation before returning it. A canceled
+ intent advances the attempt number and receives a new deterministic Stripe
+ idempotency key.
+ - A succeeded reserved intent is recorded before another checkout may begin.
+ If that payment was already recorded and its entitlement was later revoked,
+ the next request advances the attempt number so the member can repay while
+ preserving the original payment.
- Payment method types are restricted to `card`. Delayed-notification bank
- payments are out of scope because the first slice does not persist pending
- PaymentIntent attempts for safe reuse.
+ payments remain out of scope for this slice.
- `dues.confirmPayment`
- Access: protected member.
- Input: `{ paymentIntentId: string }`.
@@ -100,8 +111,8 @@ history for future admin/alumni surfaces.
session.
- Records only succeeded payments. Processing returns a processing status.
Canceled/failed/incomplete statuses return a safe `BAD_REQUEST`.
- - Uses idempotent insert behavior keyed by `stripePaymentIntentId` and the
- existing member/year uniqueness.
+ - Uses idempotent payment inserts keyed by `stripePaymentIntentId` and upserts
+ one entitlement for the member/current-year pair.
- `dues.getPaymentDates` remains deferred until admin analytics are reintroduced.
- `/api/membership` is the only new REST route because Stripe webhooks require an
HTTP protocol boundary; it must not own separate business behavior.
@@ -116,23 +127,37 @@ history for future admin/alumni surfaces.
## Data / migration / compatibility
-- Add `active boolean not null default true` to
- `knight_hacks_dues_payment`. Existing production rows backfill to active so
- current dues data keeps working after migration.
- Add nullable `stripe_payment_intent_id varchar(255)` with a unique constraint.
Legacy/manual rows can remain null while new Stripe rows become idempotent.
-- Preserve the existing unique member/year constraint.
+- `DuesPayment` is immutable recorded payment history. New rows come only from
+ Stripe; legacy manual rows remain preserved. Remove its `active` field and
+ member/year uniqueness so a revoked member can make another real payment for
+ the same academic year without overwriting the original transaction.
+- Add `DuesEntitlement` with one row per member/year, an active flag, and an
+ optional `sourcePaymentId` link. Current dues status reads only the active
+ current-year entitlement.
+- Add `DuesCheckoutReservation` with one row per member/year. Before a
+ `DuesPayment` exists, it records the Stripe PaymentIntent that repeated or
+ parallel checkout requests must reuse. It also stores a monotonically
+ increasing attempt number so a canceled or already-recorded intent can be
+ replaced. Checkout setup uses a PostgreSQL transaction-scoped advisory lock
+ and a deterministic Stripe idempotency key derived from member, year, and
+ attempt number so repeated or parallel requests converge on one PaymentIntent.
+- Activating an already-active entitlement must not replace its
+ `sourcePaymentId`; this preserves which payment funded the entitlement when
+ duplicate fulfillment paths race.
- Keep `year integer` as the academic school-year start year, e.g. `2026` for
`2026-2027`.
-- Update shared dues amount constants so new manual inserts and Stripe inserts
- both store cents (`2500` for `$25`).
-- Normalize historical `amount = 25` rows to `2500` in the migration because the
- existing manual dues constant used dollars while Stripe used cents. The current
- member-facing status does not depend on amount, but normalizing now keeps
- future admin/history reads coherent.
-- If an inactive row already exists for the current base academic year, the
- payable year becomes the next academic year. The inactive row remains history;
- the new payment gets a separate row and does not overwrite/delete history.
+- New Stripe payment rows store cents (`2500` for `$25`). Migration 0011 already
+ normalized historical `amount = 25` rows to `2500`; entitlement state does
+ not depend on amount.
+- Backfill one entitlement per normalized member/year from existing payment
+ rows. Preserve the aggregate active state and link the entitlement to the
+ newest active payment, or newest payment when all rows are inactive.
+- Normalize legacy January-July calendar-year values to the academic year that
+ began the previous August before backfilling entitlements.
+- Checkout always targets the current academic year. An inactive entitlement
+ never advances a payment into the next year.
- Add a singleton `DuesConfiguration` row keyed by `global`, with
`paymentsEnabled boolean not null default false`. Missing configuration is
also treated as paused so a partial deployment cannot accidentally open
@@ -152,8 +177,8 @@ Would this require a developer change next year?
- The `$25` price is still code-configured. That is acceptable for this first
slice because the human explicitly said the price is always `$25`, while admin
configurable pricing is out of scope.
-- Active/stale rollover requires future admin UI, but the database/API model in
- this slice preserves the state needed for that admin-controlled path.
+- Academic-year membership state is admin-controllable through entitlements;
+ payment transactions remain unchanged by grants and revocations.
- Payment availability no longer requires a developer change because authorized
member editors can change the persisted setting from `/admin/members`.
@@ -186,10 +211,11 @@ Would this require a developer change next year?
- `packages/validators/src/tests/dues.test.ts`
- academic-year label boundaries
- late-year warning boundaries
- - payable-year bump when current-year dues are stale
- `packages/api/src/tests/dues/router.test.ts`
- - status: unpaid, paid active, stale current-year unpaid/payable next year
- - create PaymentIntent: no member, duplicate paid, Stripe metadata/options
+ - status: unpaid without entitlement, paid with an active current-year
+ entitlement, inactive current-year entitlement remains unpaid
+ - create PaymentIntent: no member, duplicate paid, Stripe metadata/options,
+ open-attempt reuse, and canceled-attempt replacement
- create PaymentIntent: admin pause rejects before Stripe
- confirm PaymentIntent: succeeded inserts, idempotent repeat, processing,
failed/incomplete, wrong member/user
@@ -209,6 +235,9 @@ Would this require a developer change next year?
- paid member visiting `/member/dues` redirects to dashboard
- no-member user is routed to signup
- mobile dashboard keeps dues compact and reachable
+- `packages/db/src/tests/dues-entitlement-migration.test.ts`
+ - payment/entitlement separation, legacy-year backfill ordering, and durable
+ checkout-reservation uniqueness
Expected commands:
diff --git a/.forge/features/member-dues-payment/status.md b/.forge/features/member-dues-payment/status.md
index df269c95b..80b63735a 100644
--- a/.forge/features/member-dues-payment/status.md
+++ b/.forge/features/member-dues-payment/status.md
@@ -6,6 +6,16 @@ Current phase: Complete
## Decision log
+- 2026-08-13: Checkout setup now reserves one Stripe PaymentIntent per
+ member/current-year in durable database state. Member/year advisory locking
+ and deterministic per-attempt idempotency keys make concurrent and repeated
+ setup requests converge; canceled or already-recorded attempts advance so a
+ revoked member can repay without rewriting payment history.
+- 2026-08-12: Issue #504 supersedes the stale-payment rollover decisions below.
+ `DuesPayment` is immutable recorded history; `DuesEntitlement` owns one
+ active/inactive membership state per member and academic year. Checkout
+ targets the current year even after revocation, and successful Stripe payment
+ activates that year's entitlement.
- 2026-08-11: Human replaced the hardcoded August 31 cutoff with an
admin-controlled payment-availability setting on `/admin/members`. The
persisted setting defaults to paused, uses existing edit-member access, and
@@ -19,7 +29,7 @@ Current phase: Complete
artifact status fields to `Complete`.
- 2026-06-26: Human selected member dues payment as the next feature before the admin member dashboard because admin member management needs real dues status/history to build on.
- 2026-06-26: Dues are yearly membership dues, valid for the Knight Hacks school-year window from August 1 through the next August 1, regardless of when the member pays.
-- 2026-06-26: Admins should choose when to roll dues forward instead of deleting old dues payment records. Reforge should retain dues history and avoid legacy's "clear all dues deletes all records" behavior.
+- 2026-06-26: Reforge should retain dues history and avoid legacy's "clear all dues deletes all records" behavior.
- 2026-06-26: First payment amount remains code-configured at `$25` / `2500` cents. Admin price configuration is out of scope for the first slice unless reverse-prompting changes that.
- 2026-06-26: Member-facing payment should use Stripe's embedded Payment Element rather than redirect-only Checkout so Blade can keep the flow inside the current design system.
- 2026-06-26: Dues status should be displayed in school-year language, for example `Paid for 2026-2027`.
@@ -31,25 +41,20 @@ Current phase: Complete
- 2026-06-26: Admin rollover and manual admin dues changes are deferred to later admin/member dashboard features.
- 2026-06-26: If a member pays between May 31 and July 31, `/member/dues` should warn that the new school year is close and they will need to pay again in the fall semester, without listing a specific date.
- 2026-06-26: Keep non-refundable payment copy in the member-facing payment UX.
-- 2026-06-26: Calendar date determines the displayed academic school year and the May 31 through July 31 late-year warning. Admin-controlled active/stale state determines whether a dues record currently counts as paid.
-- 2026-06-26: A stale dues payment for the current displayed academic year should be eligible to roll/bump forward to the next academic year rather than forcing duplicate payment history for the same stale current-year record. This needs SRD-level precision before implementation.
+- 2026-06-26: Calendar date determines the displayed academic school year and the May 31 through July 31 late-year warning.
- 2026-06-26: Stripe payment options should be similar to legacy Blade, including card plus bank account behavior where practical.
- 2026-06-26: The member dashboard should avoid red/error styling for unpaid dues. Use neutral faded gray for unpaid and green for paid.
- 2026-06-26: Paid members who visit `/member/dues` should be routed back to `/member/dashboard` instead of seeing the payment page.
- 2026-06-26: After successful payment, `/member/dues` should show a short success state/dialog and route back to `/member/dashboard` after about five seconds.
-- 2026-06-26: Use `active` for dues records that currently count toward paid status. The migration should default existing dues rows to `active = true` for backporting old dues data.
- 2026-06-26: Dues `year` should mean the academic school-year start year, for example `2026` for `2026-2027`.
-- 2026-06-26: Stale current-year dues bumping should only happen after Stripe succeeds, not when the member merely opens or starts `/member/dues`.
-- 2026-06-26: If an active dues record exists for the current academic school year, the member is paid. If only a stale record exists, the member is unpaid and can pay again.
+- 2026-06-26: A member is paid only when an active entitlement exists for the current academic school year.
- 2026-06-26: The late-year warning should be a dialog on `/member/dues` entry with dismiss and return-home actions, not a permanent warning card.
-- 2026-06-26: Current `knight_hacks_dues_payment` only has `id`, `member_id`, `amount`, `payment_date`, and `year`, with a unique constraint on `(member_id, year)`. It does not currently store a Stripe PaymentIntent id.
- 2026-06-26: Legacy/current constants are mixed: `MEMBERSHIP_PRICE = 2500`, while manual admin dues used `DUES_PAYMENT = 25`. Reforge should standardize new Stripe dues writes around cents and explicitly decide whether to normalize historical `25` rows.
- 2026-06-26: Implementation default: normalize legacy/manual `amount = 25` rows to `2500` during migration so dues history uses cents consistently.
- 2026-06-26: Implementation default: add nullable unique `stripePaymentIntentId` for Stripe idempotency while keeping legacy/manual rows valid with null.
-- 2026-06-26: Stale current-year dues are preserved as history. A new payment targets the next academic start year instead of mutating the stale row or deleting history.
- 2026-06-26: Automated E2E tests may use a deterministic test-payment button in Blade E2E mode. Unit/API tests still cover Stripe PaymentIntent creation, retrieval, metadata, and error handling.
- 2026-06-27: `/member/dues` now creates the Stripe PaymentIntent server-side in normal mode and passes the initial `clientSecret` into the client component, with a visible retryable setup error fallback instead of an indefinite skeleton.
-- 2026-06-26: Coverage hardening closed every partial test-artifact mapping. Payment ownership now validates both authenticated user and member metadata before recording, repeated webhook delivery exercises the real idempotent helper, and stale dues history is verified through the browser and database together.
+- 2026-06-26: Coverage hardening closed every partial test-artifact mapping. Payment ownership validates both authenticated user and member metadata before recording, and repeated webhook delivery exercises the real idempotent helper.
- 2026-06-27: Restored the complete implementation from stash `wip: member dues payment before main fix`; the source stash remains intact as a backup.
- 2026-06-27: Audit fixed a confirmation-boundary gap: processing PaymentIntents now require the same member/user metadata ownership check as succeeded PaymentIntents.
- 2026-06-27: Audit found that the app does not persist or reuse an in-progress PaymentIntent. This is unsafe with US bank account payments because ACH can remain processing for multiple business days while a page reload can create another PaymentIntent. Human decision required: make the first slice card-only, or add durable payment-attempt state and reuse.
@@ -85,6 +90,14 @@ Current phase: Complete
- [x] Add the payment-availability control to `/admin/members`.
- [x] Generate and verify the dues configuration migration.
- [x] Run targeted and repository validation for the admin-controlled setting.
+- [x] Separate immutable payment history from yearly membership entitlement.
+- [x] Make checkout, admin controls, forms, events, check-in, and analytics read
+ `DuesEntitlement` as the membership source of truth.
+- [x] Backfill entitlements and remove payment activity state in migration 0041.
+- [x] Add focused regression coverage for payment-history preservation and
+ current-year repayment.
+- [x] Persist and serialize Stripe checkout reservations so parallel setup
+ cannot create duplicate chargeable PaymentIntents.
## Validation / commands
@@ -163,9 +176,31 @@ Current phase: Complete
- 2026-08-11: Final repository gates passed: `pnpm format` (20 tasks),
`pnpm lint` (27 tasks, warnings only), and `pnpm typecheck` (29 tasks).
- 2026-08-11: Focused admin dues-configuration API tests passed, 3 tests.
+- 2026-08-12: `pnpm db:generate`: passed after migration 0041 was generated;
+ the final rerun reported no schema drift.
+- 2026-08-12: Focused DB migration/sanitizer/lineage suites passed, 3 files / 19
+ tests. The real forms integration suite also applied all migrations to a
+ disposable PostgreSQL database and passed, 5 tests.
+- 2026-08-12: Focused API dues/admin/analytics/event suites passed, 6 files / 60
+ tests. Focused validator suites passed, 2 files / 9 tests; focused Blade
+ suites passed, 4 files / 16 tests.
+- 2026-08-12: Four focused Playwright regressions passed against a disposable
+ PostgreSQL database: admin revoke/re-grant, bulk current-year invalidation,
+ member checkout, and current-year repayment with immutable payment history.
+- 2026-08-12: `pnpm analyze:react:changed` passed, 12 files / 6 components / 0
+ failures. Repository gates passed: `pnpm format` (20 tasks), `pnpm lint` (27
+ tasks, warnings only), and `pnpm typecheck` (29 tasks).
+- 2026-08-13: `pnpm db:generate` generated migration `0042` for durable dues
+ checkout reservations.
+- 2026-08-13: Focused dues-router and club-analytics suites passed, 2 files / 30
+ tests. Targeted API, DB, and Blade typechecks passed; touched-file lint passed
+ with warnings only.
+- 2026-08-13: `pnpm db:migrate` applied the checkout migrations to the
+ disposable development database. The focused concurrent-payment Playwright
+ regression then passed, 1 test.
## Links
- PRs:
-- Issues:
+- Issues: #504
- Discord/thread context:
diff --git a/.forge/features/member-dues-payment/test-cases.md b/.forge/features/member-dues-payment/test-cases.md
index 4a0dcb56f..8ae612dde 100644
--- a/.forge/features/member-dues-payment/test-cases.md
+++ b/.forge/features/member-dues-payment/test-cases.md
@@ -47,8 +47,9 @@ pnpm --filter=@forge/blade e2e
deterministic E2E payment mode.
- Repeated webhook delivery exercises the real shared dues-recording helper and
proves that the same PaymentIntent inserts only one dues row.
-- Stale dues coverage runs from the seeded database row through API status and
- dashboard rendering, then verifies that the historical row remains unchanged.
+- Inactive-entitlement coverage runs from the seeded database row through API
+ status and dashboard rendering, then verifies that payment history remains
+ unchanged.
## Test cases
@@ -83,29 +84,29 @@ Expected observations:
- May 31 and July 31 return true.
- May 30 and August 1 return false.
-### TC-003: Stale current-year dues bump the payable year forward
+### TC-003: Inactive entitlement does not advance checkout to a future year
Setup:
- Current academic year is `2026`.
-- A member has a `DuesPayment` row for `2026` with `active = false`.
+- A member has payment history and an inactive entitlement for `2026`.
Action:
-- Compute dues status/payable year.
+- Compute dues status and begin checkout.
Expected observations:
- Member is unpaid.
-- Payable year is `2027`.
-- The stale `2026` row is not deleted or overwritten.
+- Checkout targets `2026`, the current academic year.
+- Existing payment history is not deleted or overwritten.
### TC-004: API status returns unpaid with neutral payment copy
Setup:
- Authenticated user has a completed member profile.
-- No active dues row exists for current/payable year.
+- No active entitlement exists for the current year.
Action:
@@ -114,15 +115,16 @@ Action:
Expected observations:
- `paid` is false.
-- Returned unpaid copy uses the current/payable academic year.
+- Returned unpaid copy uses the current academic year.
- The procedure does not throw for a valid member.
-### TC-005: API status returns paid for an active dues row
+### TC-005: API status returns paid for an active entitlement
Setup:
- Authenticated user has a completed member profile.
-- An active dues row exists for the current academic year.
+- An active entitlement exists for the current academic year and references a
+ Stripe payment.
Action:
@@ -133,12 +135,32 @@ Expected observations:
- `paid` is true.
- The output includes the paid year label, amount, and payment date.
-### TC-006: API status treats stale current-year row as unpaid
+### TC-005A: API status returns paid for a manual entitlement
Setup:
- Authenticated user has a completed member profile.
-- Only an inactive dues row exists for the current academic year.
+- An active entitlement exists for the current academic year with no source
+ payment.
+
+Action:
+
+- Call `dues.getStatus` and open `/member/dues`.
+
+Expected observations:
+
+- `paid` is true even though no `DuesPayment` row is linked.
+- Payment-specific fields are null and the paid date uses the entitlement's
+ update timestamp.
+- `/member/dues` redirects to the member dashboard instead of offering
+ checkout.
+
+### TC-006: API status treats an inactive current-year entitlement as unpaid
+
+Setup:
+
+- Authenticated user has a completed member profile.
+- Payment history exists, but the current-year entitlement is inactive.
Action:
@@ -147,7 +169,7 @@ Action:
Expected observations:
- `paid` is false.
-- Payable year is the next academic start year.
+- The current academic year remains the checkout target.
### TC-007: PaymentIntent creation requires a member profile
@@ -164,11 +186,11 @@ Expected observations:
- Procedure throws `NOT_FOUND`.
- Stripe is not called.
-### TC-008: PaymentIntent creation blocks duplicate active dues
+### TC-008: PaymentIntent creation blocks a duplicate entitlement
Setup:
-- Authenticated member already has an active dues row for the payable year.
+- Authenticated member already has an active current-year entitlement.
Action:
@@ -196,6 +218,28 @@ Expected observations:
- Metadata includes `member_id`, `user_id`, and `academic_year_start`.
- API returns `clientSecret`, PaymentIntent id, amount, and year label.
+### TC-009A: Repeated checkout setup reuses a durable PaymentIntent
+
+Setup:
+
+- An unpaid member has payments enabled.
+- No checkout reservation exists for the current academic year.
+
+Action:
+
+- Start checkout twice, including two requests that overlap.
+- Cancel the reserved PaymentIntent and start checkout again.
+
+Expected observations:
+
+- The initial requests use the same member/year reservation, attempt number,
+ Stripe idempotency key, and PaymentIntent.
+- No more than one chargeable PaymentIntent exists for an attempt.
+- After cancellation, the attempt number advances and Stripe creates one new
+ PaymentIntent.
+- If an earlier succeeded payment was recorded and its entitlement was later
+ revoked, checkout advances to a new attempt without changing the old payment.
+
### TC-010: Confirming a succeeded PaymentIntent records dues idempotently
Setup:
@@ -209,8 +253,10 @@ Action:
Expected observations:
-- A `DuesPayment` row is inserted with amount in cents, `active = true`, and
+- An immutable `DuesPayment` row is inserted with amount in cents and
`stripePaymentIntentId`.
+- The matching member/current-year entitlement is created or reactivated and
+ linked to the payment.
- Calling the procedure again returns paid status without duplicating rows.
### TC-011: Confirming a processing PaymentIntent does not insert dues
@@ -326,13 +372,14 @@ Expected observations:
action to return to the dashboard immediately.
- Using the immediate action returns the browser to `/member/dashboard`.
- Dashboard shows paid dues status.
-- Database contains one active dues row with amount `2500`.
+- Database contains one payment with amount `2500` and one active current-year
+ entitlement linked to it.
### TC-018: E2E paid member is redirected away from `/member/dues`
Setup:
-- Seed an E2E member with an active dues row.
+- Seed an E2E member with an active current-year entitlement.
Action:
@@ -418,11 +465,11 @@ Expected observations:
- The value is `2500`, matching Stripe cents semantics.
-### TC-NEG-002: Paid status never depends on stale rows alone
+### TC-NEG-002: Paid status never depends on payment history alone
Setup:
-- A member has only inactive dues rows.
+- A member has payment history but no active current-year entitlement.
Action:
diff --git a/apps/blade/src/app/_components/admin/members/member-detail-dialog.tsx b/apps/blade/src/app/_components/admin/members/member-detail-dialog.tsx
index 93f108202..d7c5a6542 100644
--- a/apps/blade/src/app/_components/admin/members/member-detail-dialog.tsx
+++ b/apps/blade/src/app/_components/admin/members/member-detail-dialog.tsx
@@ -1196,7 +1196,7 @@ export function MemberDetailDialog({
{detail.duesStatus.paid
? `Paid for ${detail.duesStatus.paymentAcademicYear.shortLabel}`
- : `Unpaid for ${detail.duesStatus.payableAcademicYear.shortLabel}`}
+ : `Unpaid for ${detail.duesStatus.currentAcademicYear.shortLabel}`}
@@ -1276,18 +1276,9 @@ export function MemberDetailDialog({
key={`${payment.year}-${payment.paidAt.toISOString()}`}
className="py-2.5 text-sm"
>
-
-
- Stored year {payment.year}
-
-
- {payment.active ? "Active" : "Inactive"}
-
-
+
+ Stored year {payment.year}
+
{formatTimestamp(payment.paidAt)} ·{" "}
{formatDuesAmount(payment.amount)} ·{" "}
diff --git a/apps/blade/src/app/_components/member/member-dashboard.tsx b/apps/blade/src/app/_components/member/member-dashboard.tsx
index 300ac3917..8dc7a8aa4 100644
--- a/apps/blade/src/app/_components/member/member-dashboard.tsx
+++ b/apps/blade/src/app/_components/member/member-dashboard.tsx
@@ -134,7 +134,7 @@ function DuesStatusTile({
? `Paid for the ${duesStatus.paymentAcademicYear.label}.`
: duesStatus.paymentsLocked
? "Dues payments are paused until further notice."
- : `Dues unpaid for the ${duesStatus.payableAcademicYear.label}.`}
+ : `Dues unpaid for the ${duesStatus.currentAcademicYear.label}.`}
diff --git a/apps/blade/src/app/_components/member/member-dues-payment.tsx b/apps/blade/src/app/_components/member/member-dues-payment.tsx
index 4a8024b35..fe87993ce 100644
--- a/apps/blade/src/app/_components/member/member-dues-payment.tsx
+++ b/apps/blade/src/app/_components/member/member-dues-payment.tsx
@@ -166,7 +166,7 @@ function PaymentSummary({ duesStatus }: { duesStatus: CurrentDuesStatus }) {
{duesStatus.amountDueLabel}
- {duesStatus.payableAcademicYear.label}
+ {duesStatus.currentAcademicYear.label}
@@ -471,7 +471,7 @@ function E2EDuesPaymentButton({
return;
}
- onPaid(duesStatus.payableAcademicYear.label);
+ onPaid(duesStatus.currentAcademicYear.label);
};
return (
@@ -566,7 +566,7 @@ export function MemberDuesPayment({
{duesStatus.paymentsLocked
? "Dues payments are paused until further notice."
- : `Complete dues for the ${duesStatus.payableAcademicYear.label}.`}
+ : `Complete dues for the ${duesStatus.currentAcademicYear.label}.`}
@@ -613,7 +613,7 @@ export function MemberDuesPayment({
{
+ await tx.execute(
+ sql`select pg_advisory_xact_lock(hashtextextended(${`blade:dues-checkout:${member.id}:${currentAcademicYear.startYear}`}, 0))`,
+ );
+ const existingEntitlement = await tx.query.DuesEntitlement.findFirst({
+ where: and(
+ eq(DuesEntitlement.memberId, member.id),
+ eq(DuesEntitlement.year, currentAcademicYear.startYear),
+ eq(DuesEntitlement.active, true),
),
- )
- .orderBy(desc(DuesPayment.paymentDate));
- const payableYear = getDuesPayableYear({
- currentAcademicYearStart: currentAcademicYear.startYear,
- hasStaleCurrentYearDues: currentRows.some((row) => !row.active),
- });
+ });
+ if (existingEntitlement) return;
- const existingActivePayment = await db.query.DuesPayment.findFirst({
- where: and(
- eq(DuesPayment.memberId, member.id),
- eq(DuesPayment.year, payableYear),
- eq(DuesPayment.active, true),
- ),
+ const paymentDate = new Date();
+ const [payment] = await tx
+ .insert(DuesPayment)
+ .values({
+ amount: MEMBER_DUES_PRICE_CENTS,
+ memberId: member.id,
+ paymentDate,
+ stripePaymentIntentId: `pi_e2e_${crypto.randomUUID()}`,
+ year: currentAcademicYear.startYear,
+ })
+ .returning({ id: DuesPayment.id });
+ if (!payment) throw new Error("Failed to record E2E dues payment.");
+ await tx
+ .insert(DuesEntitlement)
+ .values({
+ active: true,
+ createdAt: paymentDate,
+ memberId: member.id,
+ sourcePaymentId: payment.id,
+ updatedAt: paymentDate,
+ year: currentAcademicYear.startYear,
+ })
+ .onConflictDoUpdate({
+ set: {
+ active: true,
+ sourcePaymentId: payment.id,
+ updatedAt: paymentDate,
+ },
+ setWhere: eq(DuesEntitlement.active, false),
+ target: [DuesEntitlement.memberId, DuesEntitlement.year],
+ });
});
- if (existingActivePayment) {
- return NextResponse.json({ paid: true });
- }
-
- await db
- .insert(DuesPayment)
- .values({
- active: true,
- amount: MEMBER_DUES_PRICE_CENTS,
- memberId: member.id,
- paymentDate: new Date(),
- stripePaymentIntentId: `pi_e2e_${crypto.randomUUID()}`,
- year: payableYear,
- })
- .onConflictDoNothing();
-
return NextResponse.json({ paid: true });
}
diff --git a/apps/blade/src/tests/admin/member-detail-dialog.test.tsx b/apps/blade/src/tests/admin/member-detail-dialog.test.tsx
index 5b905f0c5..a1544f6c7 100644
--- a/apps/blade/src/tests/admin/member-detail-dialog.test.tsx
+++ b/apps/blade/src/tests/admin/member-detail-dialog.test.tsx
@@ -77,7 +77,6 @@ const detail = {
},
duesHistory: [
{
- active: true,
amount: 2500,
paidAt: new Date("2026-06-27T12:00:00Z"),
source: "Stripe",
@@ -87,11 +86,12 @@ const detail = {
duesStatus: {
amountDueLabel: "$25.00",
amountPaid: 2500,
+ currentAcademicYear: { shortLabel: "2025-2026" },
paid: true,
paidAt: new Date("2026-06-27T12:00:00Z"),
- payableAcademicYear: { shortLabel: "2025-2026" },
paymentAcademicYear: { shortLabel: "2025-2026" },
},
+ graduated: false,
employment: [
{
city: {
diff --git a/apps/blade/src/tests/e2e/admin-club-analytics.spec.ts b/apps/blade/src/tests/e2e/admin-club-analytics.spec.ts
index 1a822fbf1..b008e2529 100644
--- a/apps/blade/src/tests/e2e/admin-club-analytics.spec.ts
+++ b/apps/blade/src/tests/e2e/admin-club-analytics.spec.ts
@@ -11,6 +11,7 @@ import {
DiscordArchiveMessage,
} from "@forge/db/schemas/discord";
import {
+ DuesEntitlement,
DuesPayment,
Event,
EventAttendee,
@@ -316,13 +317,23 @@ async function seed() {
})),
);
const currentYear = getDuesAcademicYear(new Date()).startYear;
- await db.insert(DuesPayment).values(
- MEMBER_IDS.slice(0, 5).map((memberId, index) => ({
+ const payments = await db
+ .insert(DuesPayment)
+ .values(
+ MEMBER_IDS.slice(0, 5).map((memberId, index) => ({
+ amount: 2500,
+ memberId,
+ paymentDate: new Date(Date.UTC(currentYear, 8 + index, 1, 12)),
+ stripePaymentIntentId: null,
+ year: currentYear,
+ })),
+ )
+ .returning({ id: DuesPayment.id, memberId: DuesPayment.memberId });
+ await db.insert(DuesEntitlement).values(
+ payments.map((payment) => ({
active: true,
- amount: 2500,
- memberId,
- paymentDate: new Date(Date.UTC(currentYear, 8 + index, 1, 12)),
- stripePaymentIntentId: null,
+ memberId: payment.memberId,
+ sourcePaymentId: payment.id,
year: currentYear,
})),
);
diff --git a/apps/blade/src/tests/e2e/admin-member-dashboard.spec.ts b/apps/blade/src/tests/e2e/admin-member-dashboard.spec.ts
index 1699d623a..b04e9d682 100644
--- a/apps/blade/src/tests/e2e/admin-member-dashboard.spec.ts
+++ b/apps/blade/src/tests/e2e/admin-member-dashboard.spec.ts
@@ -9,6 +9,7 @@ import { db } from "@forge/db/client";
import { Permissions, Roles, Session, User } from "@forge/db/schemas/auth";
import {
DuesConfiguration,
+ DuesEntitlement,
DuesPayment,
FormResponse,
FormSections,
@@ -336,7 +337,6 @@ async function seedE2EData() {
const currentYear = getDuesAcademicYear(new Date()).startYear;
await db.insert(DuesPayment).values([
{
- active: true,
amount: 2500,
id: "00000000-0000-4000-8000-000000000810",
memberId: ALICE_MEMBER_ID,
@@ -345,7 +345,6 @@ async function seedE2EData() {
year: currentYear,
},
{
- active: true,
amount: 2600,
id: "00000000-0000-4000-8000-000000000812",
memberId: CHARLIE_MEMBER_ID,
@@ -354,7 +353,6 @@ async function seedE2EData() {
year: currentYear,
},
{
- active: true,
amount: 2500,
id: "00000000-0000-4000-8000-000000000813",
memberId: CHARLIE_MEMBER_ID,
@@ -363,7 +361,6 @@ async function seedE2EData() {
year: currentYear + 1,
},
{
- active: true,
amount: 2500,
id: "00000000-0000-4000-8000-000000000811",
memberId: BOB_MEMBER_ID,
@@ -372,6 +369,36 @@ async function seedE2EData() {
year: currentYear - 1,
},
]);
+ await db.insert(DuesEntitlement).values([
+ {
+ active: true,
+ id: "00000000-0000-4000-8000-000000000820",
+ memberId: ALICE_MEMBER_ID,
+ sourcePaymentId: "00000000-0000-4000-8000-000000000810",
+ year: currentYear,
+ },
+ {
+ active: true,
+ id: "00000000-0000-4000-8000-000000000822",
+ memberId: CHARLIE_MEMBER_ID,
+ sourcePaymentId: "00000000-0000-4000-8000-000000000812",
+ year: currentYear,
+ },
+ {
+ active: true,
+ id: "00000000-0000-4000-8000-000000000823",
+ memberId: CHARLIE_MEMBER_ID,
+ sourcePaymentId: "00000000-0000-4000-8000-000000000813",
+ year: currentYear + 1,
+ },
+ {
+ active: true,
+ id: "00000000-0000-4000-8000-000000000821",
+ memberId: BOB_MEMBER_ID,
+ sourcePaymentId: "00000000-0000-4000-8000-000000000811",
+ year: currentYear - 1,
+ },
+ ]);
}
async function signInAs(page: Page, userId: string, callbackURL = ADMIN_PATH) {
@@ -413,17 +440,17 @@ test.describe("admin member dashboard", () => {
test.beforeEach(async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
existingDuesSnapshot = await db
- .select({ active: DuesPayment.active, id: DuesPayment.id })
- .from(DuesPayment);
+ .select({ active: DuesEntitlement.active, id: DuesEntitlement.id })
+ .from(DuesEntitlement);
await seedE2EData();
});
test.afterEach(async () => {
for (const row of existingDuesSnapshot) {
await db
- .update(DuesPayment)
+ .update(DuesEntitlement)
.set({ active: row.active })
- .where(eq(DuesPayment.id, row.id));
+ .where(eq(DuesEntitlement.id, row.id));
}
});
@@ -636,6 +663,63 @@ test.describe("admin member dashboard", () => {
}
});
+ test("keeps admin dues re-grants in the current academic year", async ({
+ page,
+ }) => {
+ const originalDues = await db.query.DuesPayment.findFirst({
+ where: eq(DuesPayment.memberId, ALICE_MEMBER_ID),
+ });
+ expect(originalDues).toMatchObject({
+ year: getDuesAcademicYear(new Date()).startYear,
+ });
+ const originalEntitlement = await db.query.DuesEntitlement.findFirst({
+ where: eq(DuesEntitlement.memberId, ALICE_MEMBER_ID),
+ });
+ expect(originalEntitlement).toMatchObject({
+ active: true,
+ sourcePaymentId: originalDues?.id,
+ year: getDuesAcademicYear(new Date()).startYear,
+ });
+
+ await signInAs(page, EDITOR_USER_ID);
+ await page
+ .getByRole("textbox", { name: "Search members" })
+ .fill("Alice Archive");
+ await page
+ .getByRole("button", { name: "Revoke dues for Alice Archive" })
+ .click();
+ await expect(page.getByText("Dues revoked.")).toBeVisible();
+ await expect
+ .poll(() =>
+ db.query.DuesEntitlement.findFirst({
+ where: eq(DuesEntitlement.id, originalEntitlement?.id ?? ""),
+ }),
+ )
+ .toMatchObject({ active: false });
+
+ await page
+ .getByRole("button", { name: "Grant dues for Alice Archive" })
+ .click();
+ await expect(page.getByText("Dues granted.")).toBeVisible();
+ await expect
+ .poll(() =>
+ db.query.DuesEntitlement.findFirst({
+ where: eq(DuesEntitlement.id, originalEntitlement?.id ?? ""),
+ }),
+ )
+ .toMatchObject({
+ active: true,
+ id: originalEntitlement?.id,
+ sourcePaymentId: originalDues?.id,
+ year: getDuesAcademicYear(new Date()).startYear,
+ });
+
+ const aliceDues = await db.query.DuesPayment.findMany({
+ where: eq(DuesPayment.memberId, ALICE_MEMBER_ID),
+ });
+ expect(aliceDues).toEqual([originalDues]);
+ });
+
test("searches fuzzily, persists table state, paginates, filters, sorts, and exports every match", async ({
page,
}) => {
@@ -749,10 +833,14 @@ test.describe("admin member dashboard", () => {
await expect(page.getByText("Dues revoked.")).toBeVisible();
await expect
.poll(async () => {
- const rows = await db.query.DuesPayment.findMany({
- where: eq(DuesPayment.memberId, CHARLIE_MEMBER_ID),
+ const rows = await db.query.DuesEntitlement.findMany({
+ where: eq(DuesEntitlement.memberId, CHARLIE_MEMBER_ID),
});
- return rows.filter((row) => row.active).length;
+ return rows.filter(
+ (row) =>
+ row.active &&
+ row.year === getDuesAcademicYear(new Date()).startYear,
+ ).length;
})
.toBe(0);
await page
@@ -761,10 +849,14 @@ test.describe("admin member dashboard", () => {
await expect(page.getByText("Dues granted.")).toBeVisible();
await expect
.poll(async () => {
- const rows = await db.query.DuesPayment.findMany({
- where: eq(DuesPayment.memberId, CHARLIE_MEMBER_ID),
+ const rows = await db.query.DuesEntitlement.findMany({
+ where: eq(DuesEntitlement.memberId, CHARLIE_MEMBER_ID),
});
- return rows.filter((row) => row.active).length;
+ return rows.filter(
+ (row) =>
+ row.active &&
+ row.year === getDuesAcademicYear(new Date()).startYear,
+ ).length;
})
.toBe(1);
@@ -838,64 +930,6 @@ test.describe("admin member dashboard", () => {
await page.getByRole("button", { name: "Remove profile picture" }).click();
await expect(page.getByText("Profile picture removed.")).toBeVisible();
- await page.getByRole("button", { name: "Revoke dues" }).click();
- await expect(page.getByText("Dues status updated.")).toBeVisible();
- const originalDues = await db.query.DuesPayment.findFirst({
- where: eq(DuesPayment.memberId, ALICE_MEMBER_ID),
- });
- expect(originalDues?.active).toBe(false);
- await expect(
- page.getByRole("button", { name: "Grant dues" }),
- ).toBeVisible();
- await page.getByRole("button", { name: "Grant dues" }).click();
- await expect
- .poll(async () => {
- const rows = await db.query.DuesPayment.findMany({
- where: eq(DuesPayment.memberId, ALICE_MEMBER_ID),
- });
- return rows.find((row) => row.active) ?? null;
- })
- .toMatchObject({
- active: true,
- amount: 2500,
- stripePaymentIntentId: null,
- });
- const grantedDues = (
- await db.query.DuesPayment.findMany({
- where: eq(DuesPayment.memberId, ALICE_MEMBER_ID),
- })
- ).find((row) => row.active);
- expect(grantedDues?.id).not.toBe(originalDues?.id);
-
- await expect(
- page.getByRole("button", { name: "Revoke dues" }),
- ).toBeVisible();
- await page.getByRole("button", { name: "Revoke dues" }).click();
- await expect
- .poll(async () =>
- db.query.DuesPayment.findFirst({
- where: eq(DuesPayment.id, grantedDues?.id ?? ""),
- }),
- )
- .toMatchObject({ active: false });
- await expect(
- page.getByRole("button", { name: "Grant dues" }),
- ).toBeVisible();
- await page.getByRole("button", { name: "Grant dues" }).click();
- await expect
- .poll(async () =>
- db.query.DuesPayment.findFirst({
- where: eq(DuesPayment.id, grantedDues?.id ?? ""),
- }),
- )
- .toMatchObject({
- active: true,
- amount: grantedDues?.amount,
- id: grantedDues?.id,
- paymentDate: grantedDues?.paymentDate,
- stripePaymentIntentId: grantedDues?.stripePaymentIntentId,
- });
-
await page.getByRole("button", { name: "Delete member" }).click();
await page
.getByLabel(/Type I am absolutely sure/)
@@ -975,23 +1009,23 @@ test.describe("admin member dashboard", () => {
).toBeVisible();
expect(
- await db.query.DuesPayment.findFirst({
- where: eq(DuesPayment.id, "00000000-0000-4000-8000-000000000810"),
+ await db.query.DuesEntitlement.findFirst({
+ where: eq(DuesEntitlement.id, "00000000-0000-4000-8000-000000000820"),
}),
).toMatchObject({ active: false });
expect(
- await db.query.DuesPayment.findFirst({
- where: eq(DuesPayment.id, "00000000-0000-4000-8000-000000000812"),
+ await db.query.DuesEntitlement.findFirst({
+ where: eq(DuesEntitlement.id, "00000000-0000-4000-8000-000000000822"),
}),
).toMatchObject({ active: false });
expect(
- await db.query.DuesPayment.findFirst({
- where: eq(DuesPayment.id, "00000000-0000-4000-8000-000000000813"),
+ await db.query.DuesEntitlement.findFirst({
+ where: eq(DuesEntitlement.id, "00000000-0000-4000-8000-000000000823"),
}),
- ).toMatchObject({ active: false });
+ ).toMatchObject({ active: true });
expect(
- await db.query.DuesPayment.findFirst({
- where: eq(DuesPayment.id, "00000000-0000-4000-8000-000000000811"),
+ await db.query.DuesEntitlement.findFirst({
+ where: eq(DuesEntitlement.id, "00000000-0000-4000-8000-000000000821"),
}),
).toMatchObject({ active: true });
});
diff --git a/apps/blade/src/tests/e2e/member-dues-payment.spec.ts b/apps/blade/src/tests/e2e/member-dues-payment.spec.ts
index ff434c754..2e3d76d71 100644
--- a/apps/blade/src/tests/e2e/member-dues-payment.spec.ts
+++ b/apps/blade/src/tests/e2e/member-dues-payment.spec.ts
@@ -6,11 +6,11 @@ import { db } from "@forge/db/client";
import { User } from "@forge/db/schemas/auth";
import {
DuesConfiguration,
+ DuesEntitlement,
DuesPayment,
Member,
} from "@forge/db/schemas/knight-hacks";
import {
- buildDuesAcademicYear,
getDuesAcademicYear,
MEMBER_DASHBOARD_PATH,
MEMBER_SIGNUP_FORM_SLUG,
@@ -96,6 +96,9 @@ function memberValues({
}
async function cleanupE2EData() {
+ await db
+ .delete(DuesEntitlement)
+ .where(inArray(DuesEntitlement.memberId, testMemberIds));
await db
.delete(DuesPayment)
.where(inArray(DuesPayment.memberId, testMemberIds));
@@ -155,12 +158,24 @@ async function seedE2EData() {
}),
]);
- await db.insert(DuesPayment).values({
+ const paymentDate = new Date("2026-06-20T12:00:00Z");
+ const [payment] = await db
+ .insert(DuesPayment)
+ .values({
+ amount: 2500,
+ memberId: PAID_MEMBER_ID,
+ paymentDate,
+ stripePaymentIntentId: "pi_e2e_paid_seed",
+ year: getDuesAcademicYear().startYear,
+ })
+ .returning({ id: DuesPayment.id });
+ if (!payment) throw new Error("Failed to seed paid dues.");
+ await db.insert(DuesEntitlement).values({
active: true,
- amount: 2500,
+ createdAt: paymentDate,
memberId: PAID_MEMBER_ID,
- paymentDate: new Date("2026-06-20T12:00:00Z"),
- stripePaymentIntentId: "pi_e2e_paid_seed",
+ sourcePaymentId: payment.id,
+ updatedAt: paymentDate,
year: getDuesAcademicYear().startYear,
});
}
@@ -304,14 +319,44 @@ test.describe("member dues payment", () => {
await expect(page.getByText("Paid for the").first()).toBeVisible();
const duesRows = await db.query.DuesPayment.findMany({
+ where: eq(DuesPayment.memberId, UNPAID_MEMBER_ID),
+ });
+
+ expect(duesRows).toHaveLength(1);
+ expect(duesRows[0]?.amount).toBe(2500);
+ });
+
+ test("serializes concurrent E2E payments for one member and year", async ({
+ page,
+ }) => {
+ await signInAs(page, UNPAID_USER_ID);
+
+ const responses = await Promise.all([
+ page.request.post("/api/e2e/dues-payment"),
+ page.request.post("/api/e2e/dues-payment"),
+ ]);
+
+ expect(responses.every((response) => response.ok())).toBe(true);
+ const currentAcademicYear = getDuesAcademicYear();
+ const payments = await db.query.DuesPayment.findMany({
where: and(
eq(DuesPayment.memberId, UNPAID_MEMBER_ID),
- eq(DuesPayment.active, true),
+ eq(DuesPayment.year, currentAcademicYear.startYear),
+ ),
+ });
+ const entitlements = await db.query.DuesEntitlement.findMany({
+ where: and(
+ eq(DuesEntitlement.memberId, UNPAID_MEMBER_ID),
+ eq(DuesEntitlement.year, currentAcademicYear.startYear),
),
});
- expect(duesRows).toHaveLength(1);
- expect(duesRows[0]?.amount).toBe(2500);
+ expect(payments).toHaveLength(1);
+ expect(entitlements).toHaveLength(1);
+ expect(entitlements[0]).toMatchObject({
+ active: true,
+ sourcePaymentId: payments[0]?.id,
+ });
});
test("redirects an already paid member away from the dues page", async ({
@@ -336,21 +381,30 @@ test.describe("member dues payment", () => {
await expect(page.getByText("Complete test payment")).toHaveCount(0);
});
- test("preserves stale dues history and exposes the next payable year", async ({
+ test("preserves payment history while inactive members repay the current year", async ({
page,
}) => {
const currentAcademicYear = getDuesAcademicYear();
- const nextAcademicYear = buildDuesAcademicYear(
- currentAcademicYear.startYear + 1,
- );
- const stalePaymentDate = new Date("2026-06-01T12:00:00Z");
-
- await db.insert(DuesPayment).values({
+ const historicalPaymentDate = new Date("2026-06-01T12:00:00Z");
+
+ const [historicalPayment] = await db
+ .insert(DuesPayment)
+ .values({
+ amount: 2500,
+ memberId: UNPAID_MEMBER_ID,
+ paymentDate: historicalPaymentDate,
+ stripePaymentIntentId: "pi_e2e_historical_seed",
+ year: currentAcademicYear.startYear,
+ })
+ .returning({ id: DuesPayment.id });
+ if (!historicalPayment)
+ throw new Error("Failed to seed historical dues payment.");
+ await db.insert(DuesEntitlement).values({
active: false,
- amount: 2500,
+ createdAt: historicalPaymentDate,
memberId: UNPAID_MEMBER_ID,
- paymentDate: stalePaymentDate,
- stripePaymentIntentId: "pi_e2e_stale_seed",
+ sourcePaymentId: historicalPayment.id,
+ updatedAt: historicalPaymentDate,
year: currentAcademicYear.startYear,
});
@@ -362,25 +416,24 @@ test.describe("member dues payment", () => {
.filter({ visible: true });
await expect(
- duesStatus.getByText(`Dues unpaid for the ${nextAcademicYear.label}.`),
+ duesStatus.getByText(`Dues unpaid for the ${currentAcademicYear.label}.`),
).toBeVisible();
await expect(
duesStatus.getByRole("link", { name: "Pay dues" }),
).toBeVisible();
- const staleRows = await db.query.DuesPayment.findMany({
+ const paymentRows = await db.query.DuesPayment.findMany({
where: and(
eq(DuesPayment.memberId, UNPAID_MEMBER_ID),
eq(DuesPayment.year, currentAcademicYear.startYear),
),
});
- expect(staleRows).toHaveLength(1);
- expect(staleRows[0]?.paymentDate).toEqual(stalePaymentDate);
- expect(staleRows[0]).toMatchObject({
- active: false,
+ expect(paymentRows).toHaveLength(1);
+ expect(paymentRows[0]?.paymentDate).toEqual(historicalPaymentDate);
+ expect(paymentRows[0]).toMatchObject({
amount: 2500,
- stripePaymentIntentId: "pi_e2e_stale_seed",
+ stripePaymentIntentId: "pi_e2e_historical_seed",
year: currentAcademicYear.startYear,
});
diff --git a/apps/blade/src/tests/member/member-dashboard.test.tsx b/apps/blade/src/tests/member/member-dashboard.test.tsx
index 723b1d4d2..0481f2c24 100644
--- a/apps/blade/src/tests/member/member-dashboard.test.tsx
+++ b/apps/blade/src/tests/member/member-dashboard.test.tsx
@@ -100,17 +100,10 @@ const paidDuesStatus = {
shortLabel: "2026-2027",
startYear: 2026,
},
- currentYearHasStaleDues: false,
lateYearWarning: false,
paid: true,
paidAt: new Date("2026-08-15T12:00:00Z"),
paymentsLocked: false,
- payableAcademicYear: {
- endYear: 2027,
- label: "2026-2027 academic school year",
- shortLabel: "2026-2027",
- startYear: 2026,
- },
paymentAcademicYear: {
endYear: 2027,
label: "2026-2027 academic school year",
diff --git a/apps/blade/src/tests/member/member-dues-page.test.tsx b/apps/blade/src/tests/member/member-dues-page.test.tsx
index 679380715..5a6b7e1bc 100644
--- a/apps/blade/src/tests/member/member-dues-page.test.tsx
+++ b/apps/blade/src/tests/member/member-dues-page.test.tsx
@@ -87,17 +87,10 @@ const duesStatus = {
shortLabel: "2026-2027",
startYear: 2026,
},
- currentYearHasStaleDues: false,
lateYearWarning: false,
paid: false,
paidAt: null,
paymentsLocked: false,
- payableAcademicYear: {
- endYear: 2027,
- label: "2026-2027 academic school year",
- shortLabel: "2026-2027",
- startYear: 2026,
- },
paymentAcademicYear: {
endYear: 2027,
label: "2026-2027 academic school year",
diff --git a/apps/blade/src/tests/member/member-dues-webhook.test.ts b/apps/blade/src/tests/member/member-dues-webhook.test.ts
index 4bba8ca78..66b4e7028 100644
--- a/apps/blade/src/tests/member/member-dues-webhook.test.ts
+++ b/apps/blade/src/tests/member/member-dues-webhook.test.ts
@@ -84,7 +84,6 @@ describe("/api/membership Stripe webhook", () => {
let storedPayment: Record | null = null;
const returning = vi.fn(() => {
storedPayment = {
- active: true,
amount: 2500,
id: "dues-payment-id",
memberId: "member-id",
@@ -95,7 +94,8 @@ describe("/api/membership Stripe webhook", () => {
return Promise.resolve([storedPayment]);
});
const onConflictDoNothing = vi.fn(() => ({ returning }));
- const values = vi.fn(() => ({ onConflictDoNothing }));
+ const onConflictDoUpdate = vi.fn(() => Promise.resolve());
+ const values = vi.fn(() => ({ onConflictDoNothing, onConflictDoUpdate }));
const insert = vi.fn(() => ({ values }));
const transaction = {
insert,
@@ -143,16 +143,23 @@ describe("/api/membership Stripe webhook", () => {
database: mocks.db,
paymentIntent,
});
- expect(insert).toHaveBeenCalledTimes(1);
+ expect(insert).toHaveBeenCalledTimes(2);
expect(values).toHaveBeenCalledWith(
expect.objectContaining({
- active: true,
amount: 2500,
memberId: "member-id",
stripePaymentIntentId: "pi_webhook",
year: 2025,
}),
);
+ expect(values).toHaveBeenCalledWith(
+ expect.objectContaining({
+ active: true,
+ memberId: "member-id",
+ sourcePaymentId: "dues-payment-id",
+ year: 2025,
+ }),
+ );
expect(storedPayment).toMatchObject({
id: "dues-payment-id",
stripePaymentIntentId: "pi_webhook",
diff --git a/docs/DATABASE-USAGE.md b/docs/DATABASE-USAGE.md
index 5bd8a5b67..f3ec2b1ee 100644
--- a/docs/DATABASE-USAGE.md
+++ b/docs/DATABASE-USAGE.md
@@ -30,15 +30,17 @@ Notes:
## Club membership, dues, and events
-| Table export | SQL table | Usage |
-| ------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `Member` | `knight_hacks_member` | Stores Blade club member profiles used for signup/profile updates, admin search/filtering, guild profiles, resumes, dues, event check-in, attendance, alumni roles, and bot leaderboards. |
-| `DuesConfiguration` | `knight_hacks_dues_configuration` | Singleton configuration for whether members may start Stripe dues payments. This is operational state, not payment history. |
-| `DuesPayment` | `knight_hacks_dues_payment` | Records yearly dues payments from Stripe/admin flows and gates dues-only event check-in and dues-paying member queries. |
-| `OtherCompanies` | `knight_hacks_companies` | Stores custom company names entered during member create/update when they are not in the constants list. |
-| `Event` | `knight_hacks_event` | Stores club and hackathon events synchronized with Discord/Google Calendar and used for listings, reminders, forms, feedback, issues, check-in, and attendance. |
-| `EventAttendee` | `knight_hacks_event_attendee` | Records club member check-ins to events for attendee lists, attendance counts, member event history, and point awards. |
-| `EventFeedback` | `knight_hacks_event_feedback` | Legacy/unused feedback table; current feedback flows appear to use dynamic `FormsSchemas` and `FormResponse` records instead. |
+| Table export | SQL table | Usage |
+| ------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `Member` | `knight_hacks_member` | Stores Blade club member profiles used for signup/profile updates, admin search/filtering, guild profiles, resumes, dues, event check-in, attendance, alumni roles, and bot leaderboards. |
+| `DuesConfiguration` | `knight_hacks_dues_configuration` | Singleton configuration for whether members may start Stripe dues payments. This is operational state, not payment history. |
+| `DuesPayment` | `knight_hacks_dues_payment` | Immutable recorded payment history. New rows come from Stripe; legacy manual rows remain preserved. Multiple rows may exist for one member/year. |
+| `DuesEntitlement` | `knight_hacks_dues_entitlement` | Stores one active/inactive membership entitlement per member and academic year. This gates dues-only forms, events, check-in, and current paid status. |
+| `DuesCheckoutReservation` | `knight_hacks_dues_checkout_reservation` | Stores the Stripe PaymentIntent reserved for one member and academic year before payment completion, so retries and concurrent checkout requests reuse it. |
+| `OtherCompanies` | `knight_hacks_companies` | Stores custom company names entered during member create/update when they are not in the constants list. |
+| `Event` | `knight_hacks_event` | Stores club and hackathon events synchronized with Discord/Google Calendar and used for listings, reminders, forms, feedback, issues, check-in, and attendance. |
+| `EventAttendee` | `knight_hacks_event_attendee` | Records club member check-ins to events for attendee lists, attendance counts, member event history, and point awards. |
+| `EventFeedback` | `knight_hacks_event_feedback` | Legacy/unused feedback table; current feedback flows appear to use dynamic `FormsSchemas` and `FormResponse` records instead. |
Notes:
@@ -46,7 +48,18 @@ Notes:
- `Member.phoneNumber` is nullable but unique.
- `DuesConfiguration` accepts only the `global` row. A missing row is treated
as payments paused, and the migration seeds that safe default.
-- `DuesPayment` is unique per `(memberId, year)`.
+- `DuesPayment` does not own current membership state and must not be mutated by
+ admin grant/revoke operations. New Stripe rows use the PaymentIntent ID as
+ their idempotency key; preserved legacy/manual rows may have no provider ID.
+- `DuesEntitlement` is unique per `(memberId, year)`. `sourcePaymentId` is
+ optional for manual admin grants and links Stripe-funded entitlements to
+ immutable payment history.
+- `DuesCheckoutReservation` is unique per `(memberId, year)`. It exists before
+ a completed `DuesPayment` and records which Stripe PaymentIntent checkout
+ requests must reuse. Its attempt number
+ advances only when the reserved PaymentIntent was canceled or already became
+ an immutable payment, allowing a revoked member to pay again without a
+ duplicate concurrent charge.
- `Event.roles` is a string array used by reminders and role-scoped event filtering.
- `Event.start_datetime`/`end_datetime` are sometimes adjusted in create/update flows; preserve existing timezone/date behavior unless the task is explicitly to change it.
- `EventAttendee` has no schema-level unique constraint on `(memberId, eventId)`; duplicate prevention lives in check-in code.
diff --git a/packages/api/src/routers/dues.ts b/packages/api/src/routers/dues.ts
index 81a26c896..6e95946a0 100644
--- a/packages/api/src/routers/dues.ts
+++ b/packages/api/src/routers/dues.ts
@@ -2,9 +2,14 @@ import type { TRPCRouterRecord } from "@trpc/server";
import { TRPCError } from "@trpc/server";
import { CLUB } from "@forge/consts";
-import { desc, eq } from "@forge/db";
+import { and, desc, eq, sql } from "@forge/db";
import { db } from "@forge/db/client";
-import { DuesPayment, Member } from "@forge/db/schemas/knight-hacks";
+import {
+ DuesCheckoutReservation,
+ DuesEntitlement,
+ DuesPayment,
+ Member,
+} from "@forge/db/schemas/knight-hacks";
import { stripe } from "@forge/utils/stripe";
import {
buildDuesAcademicYear,
@@ -40,27 +45,34 @@ async function getMemberForSession(userId: string) {
return member;
}
-async function getDuesRowsForMember(memberId: string) {
- return await db
- .select({
- active: DuesPayment.active,
- amount: DuesPayment.amount,
- id: DuesPayment.id,
- paymentDate: DuesPayment.paymentDate,
- stripePaymentIntentId: DuesPayment.stripePaymentIntentId,
- year: DuesPayment.year,
- })
- .from(DuesPayment)
- .where(eq(DuesPayment.memberId, memberId))
- .orderBy(desc(DuesPayment.paymentDate));
+async function getDuesStateForMember(memberId: string) {
+ const [entitlements, payments] = await Promise.all([
+ db
+ .select()
+ .from(DuesEntitlement)
+ .where(eq(DuesEntitlement.memberId, memberId)),
+ db
+ .select({
+ amount: DuesPayment.amount,
+ id: DuesPayment.id,
+ paymentDate: DuesPayment.paymentDate,
+ stripePaymentIntentId: DuesPayment.stripePaymentIntentId,
+ year: DuesPayment.year,
+ })
+ .from(DuesPayment)
+ .where(eq(DuesPayment.memberId, memberId))
+ .orderBy(desc(DuesPayment.paymentDate)),
+ ]);
+
+ return { entitlements, payments };
}
async function getStatusForMember(memberId: string) {
- const [duesRows, paymentsEnabled] = await Promise.all([
- getDuesRowsForMember(memberId),
+ const [duesState, paymentsEnabled] = await Promise.all([
+ getDuesStateForMember(memberId),
getDuesPaymentsEnabled(),
]);
- const status = buildDuesStatus({ duesRows });
+ const status = buildDuesStatus(duesState);
return {
...status,
@@ -68,6 +80,134 @@ async function getStatusForMember(memberId: string) {
};
}
+function checkoutLockKey(memberId: string, year: number) {
+ return `blade:dues-checkout:${memberId}:${year}`;
+}
+
+function checkoutIdempotencyKey(
+ memberId: string,
+ year: number,
+ attemptNumber: number,
+) {
+ return `blade:dues-checkout:${memberId}:${year}:${attemptNumber}`;
+}
+
+async function createOrReusePaymentIntent({
+ academicYearStart,
+ member,
+}: {
+ academicYearStart: number;
+ member: Awaited>;
+}) {
+ return await db.transaction(async (tx) => {
+ await tx.execute(
+ sql`select pg_advisory_xact_lock(hashtextextended(${checkoutLockKey(member.id, academicYearStart)}, 0))`,
+ );
+
+ const activeEntitlement = await tx.query.DuesEntitlement.findFirst({
+ where: and(
+ eq(DuesEntitlement.memberId, member.id),
+ eq(DuesEntitlement.year, academicYearStart),
+ eq(DuesEntitlement.active, true),
+ ),
+ columns: { id: true },
+ });
+ if (activeEntitlement) {
+ throw new TRPCError({
+ code: "CONFLICT",
+ message: "Membership dues have already been paid.",
+ });
+ }
+
+ const checkoutReservation =
+ await tx.query.DuesCheckoutReservation.findFirst({
+ where: and(
+ eq(DuesCheckoutReservation.memberId, member.id),
+ eq(DuesCheckoutReservation.year, academicYearStart),
+ ),
+ });
+ let attemptNumber = checkoutReservation?.attemptNumber ?? 0;
+
+ if (checkoutReservation?.stripePaymentIntentId) {
+ // Keep the member/year lock while checking Stripe so concurrent setup
+ // requests cannot both decide the reservation needs a replacement.
+ const existingIntent = await stripe.paymentIntents.retrieve(
+ checkoutReservation.stripePaymentIntentId,
+ );
+ const recordedPayment =
+ existingIntent.status === "succeeded"
+ ? await tx.query.DuesPayment.findFirst({
+ where: eq(DuesPayment.stripePaymentIntentId, existingIntent.id),
+ columns: { id: true },
+ })
+ : null;
+
+ if (
+ existingIntent.status !== "canceled" &&
+ !(existingIntent.status === "succeeded" && recordedPayment)
+ ) {
+ return existingIntent;
+ }
+
+ attemptNumber += 1;
+ }
+
+ if (checkoutReservation) {
+ await tx
+ .update(DuesCheckoutReservation)
+ .set({
+ attemptNumber,
+ stripePaymentIntentId: null,
+ updatedAt: new Date(),
+ })
+ .where(eq(DuesCheckoutReservation.id, checkoutReservation.id));
+ } else {
+ await tx.insert(DuesCheckoutReservation).values({
+ attemptNumber,
+ memberId: member.id,
+ year: academicYearStart,
+ });
+ }
+
+ const paymentIntent = await stripe.paymentIntents.create(
+ {
+ amount: CLUB.MEMBERSHIP_PRICE,
+ currency: "usd",
+ metadata: {
+ academic_year_start: String(academicYearStart),
+ member_id: member.id,
+ user_id: member.userId,
+ },
+ payment_method_types: ["card"],
+ receipt_email: member.email,
+ },
+ {
+ idempotencyKey: checkoutIdempotencyKey(
+ member.id,
+ academicYearStart,
+ attemptNumber,
+ ),
+ },
+ );
+
+ await tx
+ .update(DuesCheckoutReservation)
+ .set({
+ stripePaymentIntentId: paymentIntent.id,
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ eq(DuesCheckoutReservation.memberId, member.id),
+ eq(DuesCheckoutReservation.year, academicYearStart),
+ eq(DuesCheckoutReservation.attemptNumber, attemptNumber),
+ ),
+ );
+
+ return paymentIntent;
+ });
+}
+
export const duesRouter = {
getStatus: protectedProcedure.query(async ({ ctx }) => {
const member = await getMemberForSession(ctx.session.user.id);
@@ -93,18 +233,24 @@ export const duesRouter = {
});
}
- const paymentIntent = await stripe.paymentIntents.create({
- amount: CLUB.MEMBERSHIP_PRICE,
- currency: "usd",
- metadata: {
- academic_year_start: String(status.payableAcademicYear.startYear),
- member_id: member.id,
- user_id: member.userId,
- },
- payment_method_types: ["card"],
- receipt_email: member.email,
+ const paymentIntent = await createOrReusePaymentIntent({
+ academicYearStart: status.currentAcademicYear.startYear,
+ member,
});
+ if (paymentIntent.status === "succeeded") {
+ await recordSucceededDuesPayment({
+ database: db,
+ expectedMemberId: member.id,
+ expectedUserId: member.userId,
+ paymentIntent,
+ });
+ throw new TRPCError({
+ code: "CONFLICT",
+ message: "Membership dues have already been paid.",
+ });
+ }
+
if (!paymentIntent.client_secret) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
@@ -116,7 +262,7 @@ export const duesRouter = {
amount: CLUB.MEMBERSHIP_PRICE,
amountLabel: formatDuesAmount(CLUB.MEMBERSHIP_PRICE),
clientSecret: paymentIntent.client_secret,
- paymentAcademicYear: status.payableAcademicYear,
+ paymentAcademicYear: status.currentAcademicYear,
paymentIntentId: paymentIntent.id,
};
}),
diff --git a/packages/api/src/routers/member-admin.ts b/packages/api/src/routers/member-admin.ts
index 301bcd497..33d8adc89 100644
--- a/packages/api/src/routers/member-admin.ts
+++ b/packages/api/src/routers/member-admin.ts
@@ -26,6 +26,7 @@ import "@forge/db/schemas/discord";
import {
Company,
DuesConfiguration,
+ DuesEntitlement,
DuesPayment,
Employment,
Event,
@@ -43,8 +44,8 @@ import {
adminMemberMassDuesInvalidationSchema,
adminMemberUpdateSchema,
formatDuesAmount,
+ getDuesAcademicYear,
graduationTermYearFromDate,
- MEMBER_DUES_PRICE_CENTS,
MEMBER_SIGNUP_FORM_ID,
memberUpdateSchema,
} from "@forge/validators";
@@ -67,10 +68,7 @@ import {
DUES_CONFIGURATION_ID,
getDuesPaymentsEnabled,
} from "../utils/dues/configuration";
-import {
- buildDuesStatus,
- getDuesPaymentIdsToInvalidate,
-} from "../utils/dues/status";
+import { buildDuesStatus } from "../utils/dues/status";
import {
assertCanEditMembers,
assertCanInvalidateMemberDues,
@@ -321,22 +319,31 @@ async function getCandidateRows(input: AdminMemberListInput) {
.where(structuredMemberConditions(input));
}
-async function getDuesRows(memberIds: string[]) {
- if (memberIds.length === 0) return [];
+async function getDuesState(memberIds: string[]) {
+ if (memberIds.length === 0) {
+ return { entitlements: [], payments: [] };
+ }
- return await db
- .select({
- active: DuesPayment.active,
- amount: DuesPayment.amount,
- id: DuesPayment.id,
- memberId: DuesPayment.memberId,
- paymentDate: DuesPayment.paymentDate,
- stripePaymentIntentId: DuesPayment.stripePaymentIntentId,
- year: DuesPayment.year,
- })
- .from(DuesPayment)
- .where(inArray(DuesPayment.memberId, memberIds))
- .orderBy(desc(DuesPayment.paymentDate));
+ const [entitlements, payments] = await Promise.all([
+ db
+ .select()
+ .from(DuesEntitlement)
+ .where(inArray(DuesEntitlement.memberId, memberIds)),
+ db
+ .select({
+ amount: DuesPayment.amount,
+ id: DuesPayment.id,
+ memberId: DuesPayment.memberId,
+ paymentDate: DuesPayment.paymentDate,
+ stripePaymentIntentId: DuesPayment.stripePaymentIntentId,
+ year: DuesPayment.year,
+ })
+ .from(DuesPayment)
+ .where(inArray(DuesPayment.memberId, memberIds))
+ .orderBy(desc(DuesPayment.paymentDate)),
+ ]);
+
+ return { entitlements, payments };
}
async function getMemberEventHistory(memberId: string) {
@@ -410,19 +417,28 @@ async function getMemberRoles(userId: string) {
function statusMapForCandidates(
candidates: CandidateRow[],
- duesRows: Awaited>,
+ duesState: Awaited>,
) {
- const rowsByMember = new Map();
- for (const row of duesRows) {
- const rows = rowsByMember.get(row.memberId) ?? [];
+ const entitlementsByMember = new Map();
+ for (const row of duesState.entitlements) {
+ const rows = entitlementsByMember.get(row.memberId) ?? [];
+ rows.push(row);
+ entitlementsByMember.set(row.memberId, rows);
+ }
+ const paymentsByMember = new Map();
+ for (const row of duesState.payments) {
+ const rows = paymentsByMember.get(row.memberId) ?? [];
rows.push(row);
- rowsByMember.set(row.memberId, rows);
+ paymentsByMember.set(row.memberId, rows);
}
return new Map(
candidates.map((candidate) => [
candidate.id,
- buildDuesStatus({ duesRows: rowsByMember.get(candidate.id) ?? [] }),
+ buildDuesStatus({
+ entitlements: entitlementsByMember.get(candidate.id) ?? [],
+ payments: paymentsByMember.get(candidate.id) ?? [],
+ }),
]),
);
}
@@ -448,10 +464,10 @@ function compareCandidates(
async function getOrderedCandidates(input: AdminMemberListInput) {
const candidates = await getCandidateRows(input);
- const duesRows = await getDuesRows(
+ const duesState = await getDuesState(
candidates.map((candidate) => candidate.id),
);
- const duesStatuses = statusMapForCandidates(candidates, duesRows);
+ const duesStatuses = statusMapForCandidates(candidates, duesState);
const duesFiltered =
input.duesStatuses.length === 0 || input.duesStatuses.length === 2
? candidates
@@ -685,7 +701,8 @@ export const memberAdminRouter = {
members: members.map((member) =>
toListItem(
member,
- duesStatuses.get(member.id) ?? buildDuesStatus({ duesRows: [] }),
+ duesStatuses.get(member.id) ??
+ buildDuesStatus({ entitlements: [], payments: [] }),
),
),
pagination: { page, pageCount, pageSize: input.pageSize, totalCount },
@@ -697,10 +714,10 @@ export const memberAdminRouter = {
.query(async ({ ctx, input }) => {
assertCanReadMembers(ctx);
const member = await findMemberOrThrow(input.memberId);
- const [discord, duesRows, employment, events, profilePicture, roles] =
+ const [discord, duesState, employment, events, profilePicture, roles] =
await Promise.all([
getDiscordEngagement(member.userId),
- getDuesRows([member.id]),
+ getDuesState([member.id]),
getMemberEmploymentHistory(member.id),
getMemberEventHistory(member.id),
getProfilePictureDownloadUrlForUser(member.userId),
@@ -711,8 +728,7 @@ export const memberAdminRouter = {
return {
discord,
- duesHistory: duesRows.map((row) => ({
- active: row.active,
+ duesHistory: duesState.payments.map((row) => ({
amount: row.amount,
paidAt: row.paymentDate,
source: row.stripePaymentIntentId
@@ -721,7 +737,7 @@ export const memberAdminRouter = {
year: row.year,
})),
member: toAdminMemberRecord(member),
- duesStatus: buildDuesStatus({ duesRows }),
+ duesStatus: buildDuesStatus(duesState),
employment,
engagement: {
distinctEventCount,
@@ -780,7 +796,8 @@ export const memberAdminRouter = {
...members.map((member) =>
memberCsvRow(
member,
- duesStatuses.get(member.id) ?? buildDuesStatus({ duesRows: [] }),
+ duesStatuses.get(member.id) ??
+ buildDuesStatus({ entitlements: [], payments: [] }),
),
),
]);
@@ -1039,38 +1056,29 @@ export const memberAdminRouter = {
message: "Member not found.",
});
}
- const duesRows = await tx
- .select({
- active: DuesPayment.active,
- amount: DuesPayment.amount,
- id: DuesPayment.id,
- paymentDate: DuesPayment.paymentDate,
- stripePaymentIntentId: DuesPayment.stripePaymentIntentId,
- year: DuesPayment.year,
- })
- .from(DuesPayment)
- .where(eq(DuesPayment.memberId, member.id))
- .orderBy(desc(DuesPayment.paymentDate));
- const status = buildDuesStatus({ duesRows });
+ const academicYear = getDuesAcademicYear();
+ const entitlement = await tx.query.DuesEntitlement.findFirst({
+ where: and(
+ eq(DuesEntitlement.memberId, member.id),
+ eq(DuesEntitlement.year, academicYear.startYear),
+ ),
+ });
if (input.paid) {
- if (status.paid) {
+ if (entitlement?.active) {
throw new TRPCError({
code: "CONFLICT",
message: "Member dues are already paid.",
});
}
- const existingPayable = duesRows.find(
- (row) => row.year === status.payableAcademicYear.startYear,
- );
- if (existingPayable) {
+ if (entitlement) {
const [reactivated] = await tx
- .update(DuesPayment)
- .set({ active: true })
+ .update(DuesEntitlement)
+ .set({ active: true, updatedAt: new Date() })
.where(
and(
- eq(DuesPayment.id, existingPayable.id),
- eq(DuesPayment.active, false),
+ eq(DuesEntitlement.id, entitlement.id),
+ eq(DuesEntitlement.active, false),
),
)
.returning();
@@ -1080,7 +1088,7 @@ export const memberAdminRouter = {
actionKey: "member.dues.granted",
actor: ctx.session.user,
metadata: {
- academicYear: status.payableAcademicYear.shortLabel,
+ academicYear: academicYear.shortLabel,
created: false,
reactivated: true,
},
@@ -1091,14 +1099,12 @@ export const memberAdminRouter = {
return reactivated;
}
const [created] = await tx
- .insert(DuesPayment)
+ .insert(DuesEntitlement)
.values({
active: true,
- amount: MEMBER_DUES_PRICE_CENTS,
memberId: member.id,
- paymentDate: new Date(),
- stripePaymentIntentId: null,
- year: status.payableAcademicYear.startYear,
+ sourcePaymentId: null,
+ year: academicYear.startYear,
})
.returning();
if (!created) throw new TRPCError({ code: "CONFLICT" });
@@ -1107,7 +1113,7 @@ export const memberAdminRouter = {
actionKey: "member.dues.granted",
actor: ctx.session.user,
metadata: {
- academicYear: status.payableAcademicYear.shortLabel,
+ academicYear: academicYear.shortLabel,
created: true,
reactivated: false,
},
@@ -1118,35 +1124,30 @@ export const memberAdminRouter = {
return created;
}
- const paymentIds = getDuesPaymentIdsToInvalidate({ duesRows });
- if (paymentIds.length === 0) {
- throw new TRPCError({
- code: "CONFLICT",
- message: "Member dues are already unpaid.",
- });
- }
- const revoked = await tx
- .update(DuesPayment)
- .set({ active: false })
+ const [revoked] = await tx
+ .update(DuesEntitlement)
+ .set({ active: false, updatedAt: new Date() })
.where(
and(
- inArray(DuesPayment.id, paymentIds),
- eq(DuesPayment.active, true),
+ eq(DuesEntitlement.memberId, member.id),
+ eq(DuesEntitlement.year, academicYear.startYear),
+ eq(DuesEntitlement.active, true),
),
)
.returning();
- if (revoked.length === 0) {
- throw new TRPCError({ code: "CONFLICT" });
+ if (!revoked) {
+ throw new TRPCError({
+ code: "CONFLICT",
+ message: "Member dues are already unpaid.",
+ });
}
await createAdminAuditEvent(
{
actionKey: "member.dues.revoked",
actor: ctx.session.user,
metadata: {
- academicYears: [
- ...new Set(revoked.map((payment) => payment.year)),
- ].sort(),
- affectedPaymentCount: revoked.length,
+ academicYear: academicYear.shortLabel,
+ affectedEntitlementCount: 1,
},
subjects: [memberAuditSubject(member)],
},
@@ -1187,43 +1188,18 @@ export const memberAdminRouter = {
try {
const operationId = randomUUID();
const affected = await db.transaction(async (tx) => {
- const rows = await tx
- .select({
- active: DuesPayment.active,
- amount: DuesPayment.amount,
- id: DuesPayment.id,
- memberId: DuesPayment.memberId,
- paymentDate: DuesPayment.paymentDate,
- stripePaymentIntentId: DuesPayment.stripePaymentIntentId,
- year: DuesPayment.year,
- })
- .from(DuesPayment)
- .orderBy(desc(DuesPayment.paymentDate));
- const byMember = new Map();
- for (const row of rows) {
- const memberRows = byMember.get(row.memberId) ?? [];
- memberRows.push(row);
- byMember.set(row.memberId, memberRows);
- }
const referenceDate = new Date();
- const referenceAcademicYear = buildDuesStatus({
- duesRows: [],
- referenceDate,
- }).currentAcademicYear;
- const effectiveIds = [...byMember.values()].flatMap((memberRows) =>
- getDuesPaymentIdsToInvalidate({
- duesRows: memberRows,
- referenceDate,
- }),
- );
- const updated =
- effectiveIds.length === 0
- ? []
- : await tx
- .update(DuesPayment)
- .set({ active: false })
- .where(inArray(DuesPayment.id, effectiveIds))
- .returning({ memberId: DuesPayment.memberId });
+ const referenceAcademicYear = getDuesAcademicYear(referenceDate);
+ const updated = await tx
+ .update(DuesEntitlement)
+ .set({ active: false, updatedAt: referenceDate })
+ .where(
+ and(
+ eq(DuesEntitlement.year, referenceAcademicYear.startYear),
+ eq(DuesEntitlement.active, true),
+ ),
+ )
+ .returning({ memberId: DuesEntitlement.memberId });
const affectedMemberIds = [
...new Set(updated.map((row) => row.memberId)),
];
diff --git a/packages/api/src/tests/admin/member-data.test.ts b/packages/api/src/tests/admin/member-data.test.ts
index c0d5480ac..0aa4f6f7e 100644
--- a/packages/api/src/tests/admin/member-data.test.ts
+++ b/packages/api/src/tests/admin/member-data.test.ts
@@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest";
import type { SQL } from "@forge/db";
import { StringChunk } from "@forge/db";
-import { getDuesPaymentIdsToInvalidate } from "../../utils/dues/status";
import { rankAdminMemberCandidates } from "../../utils/member/admin";
import {
graduatedCondition,
@@ -106,49 +105,4 @@ describe("admin member data utilities", () => {
hasGraduated(new Date("2027-05-02T00:00:00.000Z"), new Date(gradDay)),
).toBe(false);
});
-
- it("invalidates every row that would keep dues effective while preserving unrelated history", () => {
- const paymentDate = new Date("2026-06-20T12:00:00Z");
- const duesRows = [
- {
- active: true,
- amount: 2500,
- id: "current-academic-year",
- paymentDate,
- stripePaymentIntentId: "pi_current",
- year: 2025,
- },
- {
- active: true,
- amount: 2500,
- id: "legacy-manual-calendar-year",
- paymentDate,
- stripePaymentIntentId: null,
- year: 2026,
- },
- {
- active: true,
- amount: 2500,
- id: "historical-row",
- paymentDate,
- stripePaymentIntentId: "pi_historical",
- year: 2024,
- },
- {
- active: true,
- amount: 2500,
- id: "unrelated-future-row",
- paymentDate,
- stripePaymentIntentId: null,
- year: 2027,
- },
- ];
-
- expect(
- getDuesPaymentIdsToInvalidate({
- duesRows,
- referenceDate: new Date("2026-06-26T12:00:00Z"),
- }),
- ).toEqual(["current-academic-year", "legacy-manual-calendar-year"]);
- });
});
diff --git a/packages/api/src/tests/analytics/report.test.ts b/packages/api/src/tests/analytics/report.test.ts
index 53ee97764..ff5ce0181 100644
--- a/packages/api/src/tests/analytics/report.test.ts
+++ b/packages/api/src/tests/analytics/report.test.ts
@@ -339,7 +339,7 @@ describe("club analytics report builder", () => {
active: true,
id: "00000000-0000-4000-8000-000000000501",
memberId: MEMBER_IDS.alex,
- paymentDate: new Date("2025-09-01T00:00:00.000Z"),
+ recordedAt: new Date("2025-09-01T00:00:00.000Z"),
year: 2025,
},
];
@@ -384,6 +384,64 @@ describe("club analytics report builder", () => {
);
});
+ it("uses the UTC dues-year boundary for current entitlement coverage", () => {
+ const members = [member(MEMBER_IDS.alex)];
+ const reportAt = (referenceDate: string, year: number) =>
+ buildClubAnalyticsReport({
+ attendances: [],
+ dues: [
+ {
+ active: true,
+ id: `dues-${year}`,
+ memberId: MEMBER_IDS.alex,
+ recordedAt: new Date(`${year}-08-01T00:00:00.000Z`),
+ year,
+ },
+ ],
+ events: [],
+ feedback: [],
+ input: defaultInput,
+ members,
+ referenceDate: new Date(referenceDate),
+ }).dues.summary.paidCount;
+
+ expect(reportAt("2026-07-31T23:59:59.999Z", 2025)).toBe(1);
+ expect(reportAt("2026-08-01T00:00:00.000Z", 2026)).toBe(1);
+ expect(reportAt("2026-08-01T03:59:59.999Z", 2026)).toBe(1);
+ });
+
+ it("uses an entitlement timestamp for manual-grant chronology", () => {
+ const recordedAt = new Date("2025-09-15T12:00:00.000Z");
+ const report = buildClubAnalyticsReport({
+ attendances: [],
+ dues: [
+ {
+ active: true,
+ id: "manual-entitlement",
+ memberId: MEMBER_IDS.alex,
+ recordedAt,
+ year: 2025,
+ },
+ ],
+ events: [],
+ feedback: [],
+ input: defaultInput,
+ members: [
+ member(MEMBER_IDS.alex),
+ member(MEMBER_IDS.blair),
+ member(MEMBER_IDS.casey),
+ member(MEMBER_IDS.drew),
+ ],
+ referenceDate: new Date("2026-07-16T12:00:00.000Z"),
+ });
+ const academicYear = report.dues.academicYears.find(
+ (row) => row.startYear === 2025,
+ );
+
+ expect(academicYear?.curve[0]?.date).toEqual(recordedAt);
+ expect(academicYear?.milestones[0]?.date).toEqual(recordedAt);
+ });
+
it("[TC-026] groups deterministic action-brief observations", () => {
const undergraduates = Array.from({ length: 5 }, (_, index) =>
member(`undergraduate-${index}`, {
@@ -449,21 +507,21 @@ describe("club analytics report builder", () => {
active: true,
id: "dues-current-1",
memberId: required(undergraduates[0], "first undergraduate").id,
- paymentDate: new Date("2025-09-01T00:00:00.000Z"),
+ recordedAt: new Date("2025-09-01T00:00:00.000Z"),
year: 2025,
},
{
active: true,
id: "dues-current-2",
memberId: required(undergraduates[1], "second undergraduate").id,
- paymentDate: new Date("2025-09-02T00:00:00.000Z"),
+ recordedAt: new Date("2025-09-02T00:00:00.000Z"),
year: 2025,
},
{
active: true,
id: "dues-previous-1",
memberId: required(graduates[0], "first graduate").id,
- paymentDate: new Date("2024-09-01T00:00:00.000Z"),
+ recordedAt: new Date("2024-09-01T00:00:00.000Z"),
year: 2024,
},
];
diff --git a/packages/api/src/tests/dues/router.test.ts b/packages/api/src/tests/dues/router.test.ts
index 286106090..d1b98c316 100644
--- a/packages/api/src/tests/dues/router.test.ts
+++ b/packages/api/src/tests/dues/router.test.ts
@@ -58,7 +58,6 @@ const member = {
};
const activeDues = {
- active: true,
amount: 2500,
id: "dues-payment-id",
paymentDate: new Date("2026-06-20T12:00:00Z"),
@@ -66,6 +65,16 @@ const activeDues = {
year: 2025,
};
+const activeEntitlement = {
+ active: true,
+ createdAt: activeDues.paymentDate,
+ id: "dues-entitlement-id",
+ memberId,
+ sourcePaymentId: activeDues.id,
+ updatedAt: activeDues.paymentDate,
+ year: 2025,
+};
+
function createCaller(currentSession: Session | null = session) {
return callerFactory({
headers: new Headers(),
@@ -84,18 +93,24 @@ function mockPaymentAvailability(paymentsEnabled: boolean) {
});
}
-function mockDuesRows(rows: unknown[]) {
- const orderBy = vi.fn().mockResolvedValue(rows);
- const where = vi.fn(() => ({ orderBy }));
- const from = vi.fn(() => ({ where }));
-
- mocks.db.select.mockReturnValue({ from });
-
- return {
- from,
- orderBy,
- where,
- };
+function mockDuesState({
+ entitlements = [],
+ payments = [],
+}: {
+ entitlements?: unknown[];
+ payments?: unknown[];
+} = {}) {
+ mocks.db.select.mockReset();
+ const entitlementWhere = vi.fn().mockResolvedValue(entitlements);
+ const paymentOrderBy = vi.fn().mockResolvedValue(payments);
+ const paymentWhere = vi.fn(() => ({ orderBy: paymentOrderBy }));
+ mocks.db.select
+ .mockReturnValueOnce({
+ from: vi.fn(() => ({ where: entitlementWhere })),
+ })
+ .mockReturnValueOnce({
+ from: vi.fn(() => ({ where: paymentWhere })),
+ });
}
function stripePaymentIntent(
@@ -120,7 +135,6 @@ function stripePaymentIntent(
function mockTransaction({
existingStripePayment = null,
- existingYearPayment = null,
insertedPayment = {
...activeDues,
id: "inserted-dues-payment-id",
@@ -129,7 +143,6 @@ function mockTransaction({
memberRow = member,
}: {
existingStripePayment?: typeof activeDues | null;
- existingYearPayment?: typeof activeDues | null;
insertedPayment?: typeof activeDues | null;
memberRow?: typeof member | null;
} = {}) {
@@ -137,12 +150,12 @@ function mockTransaction({
.fn()
.mockResolvedValue(insertedPayment ? [insertedPayment] : []);
const onConflictDoNothing = vi.fn(() => ({ returning }));
- const values = vi.fn(() => ({ onConflictDoNothing }));
+ const onConflictDoUpdate = vi.fn((_options: { setWhere?: unknown }) =>
+ Promise.resolve(undefined),
+ );
+ const values = vi.fn(() => ({ onConflictDoNothing, onConflictDoUpdate }));
const insert = vi.fn(() => ({ values }));
- const duesPaymentFindFirst = vi
- .fn()
- .mockResolvedValueOnce(existingStripePayment)
- .mockResolvedValueOnce(existingYearPayment);
+ const duesPaymentFindFirst = vi.fn().mockResolvedValue(existingStripePayment);
const tx = {
insert,
query: {
@@ -163,11 +176,53 @@ function mockTransaction({
return {
duesPaymentFindFirst,
insert,
+ onConflictDoUpdate,
returning,
values,
};
}
+function mockCheckoutTransaction({
+ activeEntitlement = null,
+ checkoutReservation = null,
+ recordedPayment = null,
+}: {
+ activeEntitlement?: { id: string } | null;
+ checkoutReservation?: {
+ attemptNumber: number;
+ id: string;
+ stripePaymentIntentId: string | null;
+ } | null;
+ recordedPayment?: { id: string } | null;
+} = {}) {
+ const insertValues = vi.fn().mockResolvedValue(undefined);
+ const updateWhere = vi.fn().mockResolvedValue(undefined);
+ const updateSet = vi.fn(() => ({ where: updateWhere }));
+ const tx = {
+ execute: vi.fn().mockResolvedValue(undefined),
+ insert: vi.fn(() => ({ values: insertValues })),
+ query: {
+ DuesCheckoutReservation: {
+ findFirst: vi.fn().mockResolvedValue(checkoutReservation),
+ },
+ DuesEntitlement: {
+ findFirst: vi.fn().mockResolvedValue(activeEntitlement),
+ },
+ DuesPayment: {
+ findFirst: vi.fn().mockResolvedValue(recordedPayment),
+ },
+ },
+ update: vi.fn(() => ({ set: updateSet })),
+ };
+
+ mocks.db.transaction.mockImplementation(
+ (callback: (txHandle: typeof tx) => unknown) =>
+ Promise.resolve(callback(tx)),
+ );
+
+ return { insertValues, tx, updateSet, updateWhere };
+}
+
describe("duesRouter", () => {
beforeEach(() => {
vi.useFakeTimers();
@@ -175,7 +230,8 @@ describe("duesRouter", () => {
vi.clearAllMocks();
mockMember();
mockPaymentAvailability(false);
- mockDuesRows([]);
+ mockDuesState();
+ mockCheckoutTransaction();
});
afterEach(() => {
@@ -192,16 +248,18 @@ describe("duesRouter", () => {
state: "unpaid",
});
expect(result.currentAcademicYear.shortLabel).toBe("2025-2026");
- expect(result.payableAcademicYear.shortLabel).toBe("2025-2026");
- expect(result.payableAcademicYear.label).toBe(
+ expect(result.currentAcademicYear.label).toBe(
"2025-2026 academic school year",
);
expect(result.lateYearWarning).toBe(true);
expect(result.paymentsLocked).toBe(true);
});
- it("returns paid status for an active current-year dues row", async () => {
- mockDuesRows([activeDues]);
+ it("returns paid status for an active current-year entitlement", async () => {
+ mockDuesState({
+ entitlements: [activeEntitlement],
+ payments: [activeDues],
+ });
const result = await createCaller().dues.getStatus();
@@ -214,30 +272,42 @@ describe("duesRouter", () => {
expect(result.stripePaymentIntentId).toBe("pi_paid");
});
- it("treats a legacy calendar-year manual grant as paid", async () => {
- const legacyManualGrant = {
- ...activeDues,
- id: "legacy-manual-grant",
+ it("returns paid status for an active manual entitlement", async () => {
+ mockDuesState({
+ entitlements: [{ ...activeEntitlement, sourcePaymentId: null }],
+ });
+
+ const result = await createCaller().dues.getStatus();
+
+ expect(result).toMatchObject({
+ amountPaid: null,
+ paid: true,
+ paidAt: activeEntitlement.updatedAt,
+ paymentId: null,
+ state: "paid",
stripePaymentIntentId: null,
- year: 2026,
- };
- mockDuesRows([legacyManualGrant]);
+ });
+ });
+
+ it("does not treat payment history without an entitlement as paid", async () => {
+ mockDuesState({ payments: [activeDues] });
const result = await createCaller().dues.getStatus();
- expect(result.paid).toBe(true);
- expect(result.paymentId).toBe(legacyManualGrant.id);
- expect(result.paymentAcademicYear.shortLabel).toBe("2026-2027");
+ expect(result.paid).toBe(false);
+ expect(result.paymentId).toBeNull();
});
- it("treats stale current-year dues as unpaid and payable next year", async () => {
- mockDuesRows([{ ...activeDues, active: false }]);
+ it("treats an inactive current-year entitlement as unpaid for the current year", async () => {
+ mockDuesState({
+ entitlements: [{ ...activeEntitlement, active: false }],
+ payments: [activeDues],
+ });
const result = await createCaller().dues.getStatus();
expect(result.paid).toBe(false);
- expect(result.currentYearHasStaleDues).toBe(true);
- expect(result.payableAcademicYear.shortLabel).toBe("2026-2027");
+ expect(result.currentAcademicYear.shortLabel).toBe("2025-2026");
expect(mocks.db.transaction).not.toHaveBeenCalled();
});
@@ -253,7 +323,7 @@ describe("duesRouter", () => {
});
it("blocks PaymentIntent creation when dues are already paid", async () => {
- mockDuesRows([activeDues]);
+ mockDuesState({ entitlements: [activeEntitlement] });
await expect(
createCaller().dues.createPaymentIntent(),
@@ -276,9 +346,11 @@ describe("duesRouter", () => {
it("creates a card PaymentIntent with cents and metadata", async () => {
mockPaymentAvailability(true);
vi.setSystemTime(new Date("2026-08-31T00:00:00.000Z"));
+ const checkout = mockCheckoutTransaction();
mocks.stripe.paymentIntents.create.mockResolvedValue(
stripePaymentIntent({
id: "pi_created",
+ status: "requires_payment_method",
}),
);
@@ -308,6 +380,116 @@ describe("duesRouter", () => {
payment_method_types: ["card"],
receipt_email: "casey@example.test",
}),
+ {
+ idempotencyKey: `blade:dues-checkout:${memberId}:2026:0`,
+ },
+ );
+ expect(checkout.tx.execute).toHaveBeenCalledOnce();
+ expect(checkout.insertValues).toHaveBeenCalledWith({
+ attemptNumber: 0,
+ memberId,
+ year: 2026,
+ });
+ });
+
+ it("reuses the reserved open PaymentIntent", async () => {
+ mockPaymentAvailability(true);
+ mockCheckoutTransaction({
+ checkoutReservation: {
+ attemptNumber: 0,
+ id: "checkout-reservation-id",
+ stripePaymentIntentId: "pi_open",
+ },
+ });
+ mocks.stripe.paymentIntents.retrieve.mockResolvedValue(
+ stripePaymentIntent({
+ client_secret: "pi_open_secret",
+ id: "pi_open",
+ status: "requires_payment_method",
+ }),
+ );
+
+ const result = await createCaller().dues.createPaymentIntent();
+
+ expect(result.paymentIntentId).toBe("pi_open");
+ expect(result.clientSecret).toBe("pi_open_secret");
+ expect(mocks.stripe.paymentIntents.create).not.toHaveBeenCalled();
+ });
+
+ it("replaces a canceled reserved PaymentIntent", async () => {
+ mockPaymentAvailability(true);
+ const checkout = mockCheckoutTransaction({
+ checkoutReservation: {
+ attemptNumber: 0,
+ id: "checkout-reservation-id",
+ stripePaymentIntentId: "pi_canceled",
+ },
+ });
+ mocks.stripe.paymentIntents.retrieve.mockResolvedValue(
+ stripePaymentIntent({
+ client_secret: null,
+ id: "pi_canceled",
+ status: "canceled",
+ }),
+ );
+ mocks.stripe.paymentIntents.create.mockResolvedValue(
+ stripePaymentIntent({
+ id: "pi_replacement",
+ status: "requires_payment_method",
+ }),
+ );
+
+ const result = await createCaller().dues.createPaymentIntent();
+
+ expect(result.paymentIntentId).toBe("pi_replacement");
+ expect(checkout.updateSet).toHaveBeenCalledWith(
+ expect.objectContaining({
+ attemptNumber: 1,
+ stripePaymentIntentId: null,
+ }),
+ );
+ expect(mocks.stripe.paymentIntents.create).toHaveBeenCalledWith(
+ expect.any(Object),
+ {
+ idempotencyKey: `blade:dues-checkout:${memberId}:2025:1`,
+ },
+ );
+ });
+
+ it("advances past a recorded succeeded intent after revocation", async () => {
+ mockPaymentAvailability(true);
+ const checkout = mockCheckoutTransaction({
+ checkoutReservation: {
+ attemptNumber: 0,
+ id: "checkout-reservation-id",
+ stripePaymentIntentId: "pi_recorded",
+ },
+ recordedPayment: { id: "recorded-payment-id" },
+ });
+ mocks.stripe.paymentIntents.retrieve.mockResolvedValue(
+ stripePaymentIntent({ id: "pi_recorded" }),
+ );
+ mocks.stripe.paymentIntents.create.mockResolvedValue(
+ stripePaymentIntent({
+ id: "pi_repayment",
+ status: "requires_payment_method",
+ }),
+ );
+
+ const result = await createCaller().dues.createPaymentIntent();
+
+ expect(result.paymentIntentId).toBe("pi_repayment");
+ expect(checkout.updateSet).toHaveBeenCalledWith(
+ expect.objectContaining({
+ attemptNumber: 1,
+ stripePaymentIntentId: null,
+ }),
+ );
+ expect(mocks.stripe.paymentIntents.create).toHaveBeenCalledWith(
+ expect.any(Object),
+ {
+ idempotencyKey: `blade:dues-checkout:${memberId}:2025:1`,
+ },
);
});
@@ -329,13 +511,14 @@ describe("duesRouter", () => {
});
expect(transaction.values).toHaveBeenCalledWith(
expect.objectContaining({
- active: true,
amount: 2500,
memberId,
stripePaymentIntentId: "pi_test",
year: 2025,
}),
);
+ const entitlementUpdate = transaction.onConflictDoUpdate.mock.calls[0]?.[0];
+ expect(entitlementUpdate?.setWhere).toBeDefined();
mocks.stripe.paymentIntents.retrieve.mockResolvedValue(
stripePaymentIntent(),
diff --git a/packages/api/src/tests/integration/forms-respondent-access.test.ts b/packages/api/src/tests/integration/forms-respondent-access.test.ts
index 06d5cf1ca..eca7733ef 100644
--- a/packages/api/src/tests/integration/forms-respondent-access.test.ts
+++ b/packages/api/src/tests/integration/forms-respondent-access.test.ts
@@ -102,12 +102,23 @@ async function seed(
memberRow(INSIDER, "insider"),
memberRow(RESPONDED, "responded"),
]);
- // `buildDuesStatus` reads the academic year the run happens in, so the row
- // has to be dated relative to now rather than to a fixed year.
- await client.insert(knightHacks.DuesPayment).values({
- amount: 1_000,
+ const paymentDate = new Date();
+ const [payment] = await client
+ .insert(knightHacks.DuesPayment)
+ .values({
+ amount: 1_000,
+ memberId: INSIDER.memberId,
+ paymentDate,
+ year: getDuesAcademicYear().startYear,
+ })
+ .returning({ id: knightHacks.DuesPayment.id });
+ if (!payment) throw new Error("Failed to seed dues payment.");
+ await client.insert(knightHacks.DuesEntitlement).values({
+ active: true,
+ createdAt: paymentDate,
memberId: INSIDER.memberId,
- paymentDate: new Date(),
+ sourcePaymentId: payment.id,
+ updatedAt: paymentDate,
year: getDuesAcademicYear().startYear,
});
diff --git a/packages/api/src/utils/analytics/club-report.ts b/packages/api/src/utils/analytics/club-report.ts
index c9dc647ee..a6b8c285e 100644
--- a/packages/api/src/utils/analytics/club-report.ts
+++ b/packages/api/src/utils/analytics/club-report.ts
@@ -1,7 +1,8 @@
import type { AnalyticsReportInput } from "@forge/validators";
-import { eq, isNull } from "@forge/db";
+import { eq, isNull, sql } from "@forge/db";
import { db } from "@forge/db/client";
import {
+ DuesEntitlement,
DuesPayment,
Event,
EventAttendee,
@@ -57,13 +58,17 @@ async function loadClubAnalyticsSources() {
.where(isNull(Event.hackathonId)),
db
.select({
- active: DuesPayment.active,
- id: DuesPayment.id,
- memberId: DuesPayment.memberId,
- paymentDate: DuesPayment.paymentDate,
- year: DuesPayment.year,
+ active: DuesEntitlement.active,
+ id: DuesEntitlement.id,
+ memberId: DuesEntitlement.memberId,
+ recordedAt: sql`coalesce(${DuesPayment.paymentDate}, ${DuesEntitlement.createdAt})`,
+ year: DuesEntitlement.year,
})
- .from(DuesPayment),
+ .from(DuesEntitlement)
+ .leftJoin(
+ DuesPayment,
+ eq(DuesPayment.id, DuesEntitlement.sourcePaymentId),
+ ),
db
.select({
answers: FormResponse.responseData,
diff --git a/packages/api/src/utils/analytics/report.ts b/packages/api/src/utils/analytics/report.ts
index 0c56c755a..347e315f8 100644
--- a/packages/api/src/utils/analytics/report.ts
+++ b/packages/api/src/utils/analytics/report.ts
@@ -6,9 +6,8 @@ import type {
AnalyticsReportInput,
} from "@forge/validators";
import { EVENTS, FORMS } from "@forge/consts";
-import { buildDuesAcademicYear } from "@forge/validators";
+import { buildDuesAcademicYear, getDuesAcademicYear } from "@forge/validators";
-import { buildDuesStatus } from "../dues/status";
import { deriveAgeBand, inferAcademicYear } from "./demographics";
const DAY_MS = 24 * 60 * 60 * 1000;
@@ -50,7 +49,7 @@ export interface AnalyticsDuesSource {
active: boolean;
id: string;
memberId: string;
- paymentDate: Date;
+ recordedAt: Date;
year: number;
}
@@ -980,24 +979,14 @@ function currentDues(
dues: readonly AnalyticsDuesSource[],
referenceDate: Date,
) {
- const rowsByMember = new Map();
- dues.forEach((row) => {
- const rows = rowsByMember.get(row.memberId) ?? [];
- rows.push(row);
- rowsByMember.set(row.memberId, rows);
- });
+ const currentYear = getDuesAcademicYear(referenceDate).startYear;
+ const paidMemberIds = new Set(
+ dues
+ .filter((row) => row.active && row.year === currentYear)
+ .map((row) => row.memberId),
+ );
return new Map(
- members.map((member) => [
- member.id,
- buildDuesStatus({
- duesRows: (rowsByMember.get(member.id) ?? []).map((row) => ({
- ...row,
- amount: 0,
- stripePaymentIntentId: null,
- })),
- referenceDate,
- }).paid,
- ]),
+ members.map((member) => [member.id, paidMemberIds.has(member.id)]),
);
}
@@ -1006,7 +995,7 @@ function academicYearHistory(
dues: readonly AnalyticsDuesSource[],
referenceDate: Date,
) {
- const currentYear = academicYearStart(referenceDate);
+ const currentYear = getDuesAcademicYear(referenceDate).startYear;
const years = new Set([
currentYear,
currentYear - 1,
@@ -1026,7 +1015,7 @@ function academicYearHistory(
.filter((row) => row.year === year)
.map((row) => [row.memberId, row] as const),
).values(),
- ].sort((a, b) => a.paymentDate.getTime() - b.paymentDate.getTime());
+ ].sort((a, b) => a.recordedAt.getTime() - b.recordedAt.getTime());
let activeCount = 0;
let staleCount = 0;
const curve = rows.map((row) => {
@@ -1034,12 +1023,11 @@ function academicYearHistory(
else staleCount += 1;
return {
activeCount,
- date: row.paymentDate,
+ date: row.recordedAt,
elapsedDays: Math.max(
0,
Math.floor(
- (row.paymentDate.getTime() -
- zonedMidnight(year, 7, 1).getTime()) /
+ (row.recordedAt.getTime() - zonedMidnight(year, 7, 1).getTime()) /
DAY_MS,
),
),
@@ -1174,7 +1162,7 @@ function duesLifecycle({
members: readonly AnalyticsMemberSource[];
referenceDate: Date;
}) {
- const currentYear = academicYearStart(referenceDate);
+ const currentYear = getDuesAcademicYear(referenceDate).startYear;
const elapsedDays = Math.max(
0,
Math.floor(
@@ -1191,8 +1179,7 @@ function duesLifecycle({
memberIds.has(row.memberId) &&
row.year === year &&
Math.floor(
- (row.paymentDate.getTime() -
- zonedMidnight(year, 7, 1).getTime()) /
+ (row.recordedAt.getTime() - zonedMidnight(year, 7, 1).getTime()) /
DAY_MS,
) <= elapsedDays,
)
diff --git a/packages/api/src/utils/dues/entitlement.ts b/packages/api/src/utils/dues/entitlement.ts
new file mode 100644
index 000000000..6db2e461a
--- /dev/null
+++ b/packages/api/src/utils/dues/entitlement.ts
@@ -0,0 +1,24 @@
+import { and, eq } from "@forge/db";
+import { db } from "@forge/db/client";
+import { DuesEntitlement } from "@forge/db/schemas/knight-hacks";
+import { getDuesAcademicYear } from "@forge/validators";
+
+import type { WriteDb } from "../db";
+
+export async function hasCurrentDuesEntitlement(
+ memberId: string,
+ database: WriteDb = db,
+ referenceDate = new Date(),
+) {
+ const academicYear = getDuesAcademicYear(referenceDate);
+ const entitlement = await database.query.DuesEntitlement.findFirst({
+ columns: { id: true },
+ where: and(
+ eq(DuesEntitlement.memberId, memberId),
+ eq(DuesEntitlement.year, academicYear.startYear),
+ eq(DuesEntitlement.active, true),
+ ),
+ });
+
+ return Boolean(entitlement);
+}
diff --git a/packages/api/src/utils/dues/payment.ts b/packages/api/src/utils/dues/payment.ts
index f2257676e..44d341bc0 100644
--- a/packages/api/src/utils/dues/payment.ts
+++ b/packages/api/src/utils/dues/payment.ts
@@ -3,7 +3,11 @@ import { TRPCError } from "@trpc/server";
import type { db as forgeDb } from "@forge/db/client";
import { and, eq } from "@forge/db";
-import { DuesPayment, Member } from "@forge/db/schemas/knight-hacks";
+import {
+ DuesEntitlement,
+ DuesPayment,
+ Member,
+} from "@forge/db/schemas/knight-hacks";
interface RecordSucceededDuesPaymentOptions {
database: typeof forgeDb;
@@ -102,13 +106,13 @@ export async function recordSucceededDuesPayment({
};
}
+ const paymentDate = new Date(paymentIntent.created * 1000);
const [insertedPayment] = await tx
.insert(DuesPayment)
.values({
- active: true,
amount: paymentIntent.amount,
memberId: metadata.memberId,
- paymentDate: new Date(paymentIntent.created * 1000),
+ paymentDate,
stripePaymentIntentId: paymentIntent.id,
year: metadata.academicYearStart,
})
@@ -116,24 +120,41 @@ export async function recordSucceededDuesPayment({
.returning();
if (insertedPayment) {
+ await tx
+ .insert(DuesEntitlement)
+ .values({
+ active: true,
+ createdAt: paymentDate,
+ memberId: metadata.memberId,
+ sourcePaymentId: insertedPayment.id,
+ updatedAt: paymentDate,
+ year: metadata.academicYearStart,
+ })
+ .onConflictDoUpdate({
+ set: {
+ active: true,
+ sourcePaymentId: insertedPayment.id,
+ updatedAt: new Date(),
+ },
+ setWhere: eq(DuesEntitlement.active, false),
+ target: [DuesEntitlement.memberId, DuesEntitlement.year],
+ });
return {
duesPayment: insertedPayment,
inserted: true,
};
}
- // A member/year conflict can happen if a webhook and client confirmation
- // race, or if an admin marks dues manually before Stripe retries.
- const existingYearPayment = await tx.query.DuesPayment.findFirst({
- where: and(
- eq(DuesPayment.memberId, metadata.memberId),
- eq(DuesPayment.year, metadata.academicYearStart),
- ),
+ // The webhook and client confirmation can race on the same PaymentIntent.
+ // The winner records both the immutable payment and its entitlement; the
+ // loser returns that payment without reactivating a later admin revocation.
+ const concurrentStripePayment = await tx.query.DuesPayment.findFirst({
+ where: eq(DuesPayment.stripePaymentIntentId, paymentIntent.id),
});
- if (existingYearPayment) {
+ if (concurrentStripePayment) {
return {
- duesPayment: existingYearPayment,
+ duesPayment: concurrentStripePayment,
inserted: false,
};
}
diff --git a/packages/api/src/utils/dues/status.ts b/packages/api/src/utils/dues/status.ts
index ecaa7d2e5..93a1c3a48 100644
--- a/packages/api/src/utils/dues/status.ts
+++ b/packages/api/src/utils/dues/status.ts
@@ -1,14 +1,20 @@
import {
- buildDuesAcademicYear,
formatDuesAmount,
getDuesAcademicYear,
- getDuesPayableYear,
isLateDuesPaymentWindow,
MEMBER_DUES_PRICE_CENTS,
} from "@forge/validators";
-export interface DuesStatusRow {
+export interface DuesEntitlementStatusRow {
active: boolean;
+ createdAt: Date;
+ id: string;
+ sourcePaymentId: string | null;
+ updatedAt: Date;
+ year: number;
+}
+
+export interface DuesPaymentStatusRow {
amount: number;
id: string;
paymentDate: Date;
@@ -17,77 +23,35 @@ export interface DuesStatusRow {
}
export function buildDuesStatus({
- duesRows,
+ entitlements,
+ payments,
referenceDate = new Date(),
}: {
- duesRows: DuesStatusRow[];
+ entitlements: DuesEntitlementStatusRow[];
+ payments: DuesPaymentStatusRow[];
referenceDate?: Date;
}) {
const currentAcademicYear = getDuesAcademicYear(referenceDate);
- const currentYearRows = duesRows.filter(
- (row) => row.year === currentAcademicYear.startYear,
+ const entitlement = entitlements.find(
+ (row) => row.year === currentAcademicYear.startYear && row.active,
);
- const activeCurrentYearPayment = currentYearRows.find((row) => row.active);
- const hasStaleCurrentYearDues = currentYearRows.some((row) => !row.active);
- const payableYearStart = getDuesPayableYear({
- currentAcademicYearStart: currentAcademicYear.startYear,
- hasStaleCurrentYearDues,
- });
- const activePayableYearPayment = duesRows.find(
- (row) => row.year === payableYearStart && row.active,
- );
- // Legacy Blade stored the calendar year for both manual grants and Stripe
- // payments. Keep those production rows effective while new writes use the
- // academic-year start.
- const activeLegacyCalendarYearPayment = duesRows.find(
- (row) => row.year === referenceDate.getUTCFullYear() && row.active,
- );
- const paidPayment =
- activeCurrentYearPayment ??
- activePayableYearPayment ??
- activeLegacyCalendarYearPayment;
- const paymentYearStart = paidPayment?.year ?? payableYearStart;
+ const sourcePayment = entitlement?.sourcePaymentId
+ ? payments.find((row) => row.id === entitlement.sourcePaymentId)
+ : undefined;
return {
amountDue: MEMBER_DUES_PRICE_CENTS,
amountDueLabel: formatDuesAmount(MEMBER_DUES_PRICE_CENTS),
- amountPaid: paidPayment?.amount ?? null,
+ amountPaid: sourcePayment?.amount ?? null,
currentAcademicYear,
- currentYearHasStaleDues: hasStaleCurrentYearDues,
lateYearWarning: isLateDuesPaymentWindow(referenceDate),
- paid: Boolean(paidPayment),
- paidAt: paidPayment?.paymentDate ?? null,
- payableAcademicYear: buildDuesAcademicYear(payableYearStart),
- paymentAcademicYear: buildDuesAcademicYear(paymentYearStart),
- paymentId: paidPayment?.id ?? null,
- state: paidPayment ? ("paid" as const) : ("unpaid" as const),
- stripePaymentIntentId: paidPayment?.stripePaymentIntentId ?? null,
+ paid: Boolean(entitlement),
+ paidAt: entitlement
+ ? (sourcePayment?.paymentDate ?? entitlement.updatedAt)
+ : null,
+ paymentAcademicYear: currentAcademicYear,
+ paymentId: sourcePayment?.id ?? null,
+ state: entitlement ? ("paid" as const) : ("unpaid" as const),
+ stripePaymentIntentId: sourcePayment?.stripePaymentIntentId ?? null,
};
}
-
-export function getDuesPaymentIdsToInvalidate({
- duesRows,
- referenceDate = new Date(),
-}: {
- duesRows: DuesStatusRow[];
- referenceDate?: Date;
-}) {
- const simulatedRows = duesRows.map((row) => ({ ...row }));
- const paymentIds: string[] = [];
-
- while (paymentIds.length < simulatedRows.length) {
- const paymentId = buildDuesStatus({
- duesRows: simulatedRows,
- referenceDate,
- }).paymentId;
- if (!paymentId || paymentIds.includes(paymentId)) break;
-
- const payment = simulatedRows.find((row) => row.id === paymentId);
- if (!payment) break;
-
- paymentIds.push(paymentId);
- payment.active = false;
- }
-
- return paymentIds;
-}
diff --git a/packages/api/src/utils/events/database-attendance.ts b/packages/api/src/utils/events/database-attendance.ts
index 99c1d2ef7..b4bc31da6 100644
--- a/packages/api/src/utils/events/database-attendance.ts
+++ b/packages/api/src/utils/events/database-attendance.ts
@@ -3,14 +3,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
import { and, eq, sql } from "@forge/db";
import { db } from "@forge/db/client";
import { Permissions } from "@forge/db/schemas/auth";
-import {
- DuesPayment,
- Event,
- EventAttendee,
- Member,
-} from "@forge/db/schemas/knight-hacks";
+import { Event, EventAttendee, Member } from "@forge/db/schemas/knight-hacks";
-import { buildDuesStatus } from "../dues/status";
+import { hasCurrentDuesEntitlement } from "../dues/entitlement";
import { eventRowToWorkflowRecord } from "./database-state";
type DbExecutor =
@@ -30,20 +25,17 @@ export function createDbAttendanceState({
where: eq(Member.id, memberId),
});
if (!member) return null;
- const [assignments, duesRows] = await Promise.all([
+ const [assignments, duesActive] = await Promise.all([
executor()
.select({ roleId: Permissions.roleId })
.from(Permissions)
.where(eq(Permissions.userId, member.userId)),
- executor()
- .select()
- .from(DuesPayment)
- .where(eq(DuesPayment.memberId, member.id)),
+ hasCurrentDuesEntitlement(member.id, executor()),
]);
return {
company: member.company,
discordUsername: member.discordUser,
- duesActive: buildDuesStatus({ duesRows }).paid,
+ duesActive,
firstName: member.firstName,
guildProfileVisible: member.guildProfileVisible,
id: member.id,
diff --git a/packages/api/src/utils/events/queries.ts b/packages/api/src/utils/events/queries.ts
index 6ea8f2506..fdfc6191f 100644
--- a/packages/api/src/utils/events/queries.ts
+++ b/packages/api/src/utils/events/queries.ts
@@ -23,7 +23,6 @@ import {
import { db } from "@forge/db/client";
import { Permissions, User } from "@forge/db/schemas/auth";
import {
- DuesPayment,
Event,
EventAttendee,
EventTag,
@@ -31,7 +30,7 @@ import {
} from "@forge/db/schemas/knight-hacks";
import type { MemberDiscoveryRecord } from "./discovery";
-import { buildDuesStatus } from "../dues/status";
+import { hasCurrentDuesEntitlement } from "../dues/entitlement";
import { toAdminEventDto } from "./admin-dto";
import { eventRowToWorkflowRecord } from "./database-state";
import { rankCheckInIdentityCandidates } from "./discovery";
@@ -359,11 +358,7 @@ export async function queryAdminEventRecords(
}
async function loadDuesStatus(memberId: string, now: Date) {
- const rows = await db
- .select()
- .from(DuesPayment)
- .where(eq(DuesPayment.memberId, memberId));
- return buildDuesStatus({ duesRows: rows, referenceDate: now }).paid;
+ return await hasCurrentDuesEntitlement(memberId, db, now);
}
export async function loadMemberDiscoveryRecord(
diff --git a/packages/api/src/utils/forms/database-responses.ts b/packages/api/src/utils/forms/database-responses.ts
index 2c3db6944..84f0ff68b 100644
--- a/packages/api/src/utils/forms/database-responses.ts
+++ b/packages/api/src/utils/forms/database-responses.ts
@@ -4,7 +4,6 @@ import { and, desc, eq, inArray } from "@forge/db";
import { db } from "@forge/db/client";
import { Permissions } from "@forge/db/schemas/auth";
import {
- DuesPayment,
FormAttachment,
FormCallbackExecution,
FormResponse,
@@ -15,7 +14,7 @@ import {
import type { PlatformFormActor } from "./actor";
import { createAdminAuditEvent } from "../audit/service";
-import { buildDuesStatus } from "../dues/status";
+import { hasCurrentDuesEntitlement } from "../dues/entitlement";
import { requireFormCapability } from "./access";
import { auditActor } from "./actor";
import { summarizeFormResponses } from "./analytics";
@@ -248,13 +247,9 @@ export async function respondentForm(
.select({ roleId: Permissions.roleId })
.from(Permissions)
.where(eq(Permissions.userId, userId));
- const duesRows = await db
- .select()
- .from(DuesPayment)
- .where(eq(DuesPayment.memberId, member.id));
const respondentContext = {
actor: {
- duesPaid: buildDuesStatus({ duesRows }).paid,
+ duesPaid: await hasCurrentDuesEntitlement(member.id),
memberId: member.id,
roleIds: assignedRoles.map(({ roleId }) => roleId),
userId,
diff --git a/packages/api/src/utils/forms/manager.ts b/packages/api/src/utils/forms/manager.ts
index 4ecdf0160..62700da21 100644
--- a/packages/api/src/utils/forms/manager.ts
+++ b/packages/api/src/utils/forms/manager.ts
@@ -9,7 +9,6 @@ import { and, eq, sql } from "@forge/db";
import { db } from "@forge/db/client";
import { Permissions } from "@forge/db/schemas/auth";
import {
- DuesPayment,
FormResponse,
FormResponseRoles,
FormSections,
@@ -21,7 +20,7 @@ import {
import { formDefinitionSchema, validateFormAnswers } from "@forge/validators";
import type { WriteDb } from "../db";
-import { buildDuesStatus } from "../dues/status";
+import { hasCurrentDuesEntitlement } from "../dues/entitlement";
import { assertAndAttachResponseFiles } from "./attachments";
export interface FormConnectionMapping {
@@ -267,11 +266,7 @@ async function assertDuesEligibility(
message: "Paid dues are required.",
});
}
- const duesRows = await database
- .select()
- .from(DuesPayment)
- .where(eq(DuesPayment.memberId, member.id));
- if (!buildDuesStatus({ duesRows }).paid) {
+ if (!(await hasCurrentDuesEntitlement(member.id, database))) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Paid dues are required.",
diff --git a/packages/db/drizzle/0041_lying_jackal.sql b/packages/db/drizzle/0041_lying_jackal.sql
new file mode 100644
index 000000000..a86664d92
--- /dev/null
+++ b/packages/db/drizzle/0041_lying_jackal.sql
@@ -0,0 +1,52 @@
+CREATE TABLE "knight_hacks_dues_checkout_reservation" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "member_id" uuid NOT NULL,
+ "year" integer NOT NULL,
+ "attempt_number" integer DEFAULT 0 NOT NULL,
+ "stripe_payment_intent_id" varchar(255),
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL,
+ CONSTRAINT "knight_hacks_dues_checkout_reservation_memberId_year_unique" UNIQUE("member_id","year"),
+ CONSTRAINT "knight_hacks_dues_checkout_reservation_stripe_payment_intent_id_unique" UNIQUE("stripe_payment_intent_id")
+);
+--> statement-breakpoint
+CREATE TABLE "knight_hacks_dues_entitlement" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "member_id" uuid NOT NULL,
+ "year" integer NOT NULL,
+ "active" boolean DEFAULT true NOT NULL,
+ "source_payment_id" uuid,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL,
+ CONSTRAINT "knight_hacks_dues_entitlement_memberId_year_unique" UNIQUE("member_id","year"),
+ CONSTRAINT "knight_hacks_dues_entitlement_source_payment_id_unique" UNIQUE("source_payment_id")
+);
+--> statement-breakpoint
+ALTER TABLE "knight_hacks_dues_payment" DROP CONSTRAINT "knight_hacks_dues_payment_memberId_year_unique";--> statement-breakpoint
+ALTER TABLE "knight_hacks_dues_checkout_reservation" ADD CONSTRAINT "knight_hacks_dues_checkout_reservation_member_id_knight_hacks_member_id_fk" FOREIGN KEY ("member_id") REFERENCES "public"."knight_hacks_member"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "knight_hacks_dues_entitlement" ADD CONSTRAINT "knight_hacks_dues_entitlement_member_id_knight_hacks_member_id_fk" FOREIGN KEY ("member_id") REFERENCES "public"."knight_hacks_member"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "knight_hacks_dues_entitlement" ADD CONSTRAINT "knight_hacks_dues_entitlement_source_payment_id_knight_hacks_dues_payment_id_fk" FOREIGN KEY ("source_payment_id") REFERENCES "public"."knight_hacks_dues_payment"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+-- Legacy Blade stored the UTC calendar year. Normalize January-July payments
+-- to the academic year that began the previous August before backfilling.
+UPDATE "knight_hacks_dues_payment"
+SET "year" = "year" - 1
+WHERE "year" = extract(year FROM "payment_date")::integer
+ AND extract(month FROM "payment_date") < 8;--> statement-breakpoint
+INSERT INTO "knight_hacks_dues_entitlement" (
+ "member_id",
+ "year",
+ "active",
+ "source_payment_id",
+ "created_at",
+ "updated_at"
+)
+SELECT
+ "member_id",
+ "year",
+ bool_or("active"),
+ (array_agg("id" ORDER BY "active" DESC, "payment_date" DESC, "id"))[1],
+ min("payment_date"),
+ max("payment_date")
+FROM "knight_hacks_dues_payment"
+GROUP BY "member_id", "year";--> statement-breakpoint
+ALTER TABLE "knight_hacks_dues_payment" DROP COLUMN "active";
diff --git a/packages/db/drizzle/meta/0041_snapshot.json b/packages/db/drizzle/meta/0041_snapshot.json
new file mode 100644
index 000000000..f85a9bce8
--- /dev/null
+++ b/packages/db/drizzle/meta/0041_snapshot.json
@@ -0,0 +1,11242 @@
+{
+ "id": "217d1b9d-3f90-4c70-b834-a2e7627002f5",
+ "prevId": "1407edd6-1ab5-4796-b287-15ac41cda6e1",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.audit_event": {
+ "name": "audit_event",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "occurred_at": {
+ "name": "occurred_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "action_key": {
+ "name": "action_key",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "outcome": {
+ "name": "outcome",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "operation_id": {
+ "name": "operation_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_user_id": {
+ "name": "actor_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_member_id": {
+ "name": "actor_member_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_discord_user_id": {
+ "name": "actor_discord_user_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_label": {
+ "name": "actor_label",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_role_label": {
+ "name": "actor_role_label",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_role_color": {
+ "name": "actor_role_color",
+ "type": "varchar(7)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "changes": {
+ "name": "changes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ }
+ },
+ "indexes": {
+ "audit_event_action_occurred_idx": {
+ "name": "audit_event_action_occurred_idx",
+ "columns": [
+ {
+ "expression": "action_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_event_actor_occurred_idx": {
+ "name": "audit_event_actor_occurred_idx",
+ "columns": [
+ {
+ "expression": "actor_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_event_actor_member_occurred_idx": {
+ "name": "audit_event_actor_member_occurred_idx",
+ "columns": [
+ {
+ "expression": "actor_member_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_event_domain_occurred_idx": {
+ "name": "audit_event_domain_occurred_idx",
+ "columns": [
+ {
+ "expression": "domain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_event_occurred_idx": {
+ "name": "audit_event_occurred_idx",
+ "columns": [
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_event_operation_idx": {
+ "name": "audit_event_operation_idx",
+ "columns": [
+ {
+ "expression": "operation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_event_outcome_occurred_idx": {
+ "name": "audit_event_outcome_occurred_idx",
+ "columns": [
+ {
+ "expression": "outcome",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "audit_event_actor_role_color_check": {
+ "name": "audit_event_actor_role_color_check",
+ "value": "\"audit_event\".\"actor_role_color\" IS NULL OR \"audit_event\".\"actor_role_color\" ~ '^#[0-9A-Fa-f]{6}$'"
+ },
+ "audit_event_outcome_check": {
+ "name": "audit_event_outcome_check",
+ "value": "\"audit_event\".\"outcome\" IN ('committed', 'partial_external')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.audit_subject": {
+ "name": "audit_subject",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "relation": {
+ "name": "relation",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_type": {
+ "name": "target_type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_id": {
+ "name": "target_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_label": {
+ "name": "target_label",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "member_id": {
+ "name": "member_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "result_outcome": {
+ "name": "result_outcome",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ }
+ },
+ "indexes": {
+ "audit_subject_event_position_idx": {
+ "name": "audit_subject_event_position_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "position",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_subject_member_event_idx": {
+ "name": "audit_subject_member_event_idx",
+ "columns": [
+ {
+ "expression": "member_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_subject_one_primary_idx": {
+ "name": "audit_subject_one_primary_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"audit_subject\".\"relation\" = 'primary'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_subject_target_event_idx": {
+ "name": "audit_subject_target_event_idx",
+ "columns": [
+ {
+ "expression": "target_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_subject_event_id_audit_event_id_fk": {
+ "name": "audit_subject_event_id_audit_event_id_fk",
+ "tableFrom": "audit_subject",
+ "tableTo": "audit_event",
+ "columnsFrom": ["event_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "audit_subject_relation_check": {
+ "name": "audit_subject_relation_check",
+ "value": "\"audit_subject\".\"relation\" IN ('primary', 'secondary', 'result')"
+ },
+ "audit_subject_result_outcome_check": {
+ "name": "audit_subject_result_outcome_check",
+ "value": "\"audit_subject\".\"result_outcome\" IS NULL OR \"audit_subject\".\"result_outcome\" IN ('succeeded', 'skipped', 'failed_external', 'failed_internal', 'compensated')"
+ },
+ "audit_subject_result_outcome_shape_check": {
+ "name": "audit_subject_result_outcome_shape_check",
+ "value": "(\"audit_subject\".\"relation\" = 'result') = (\"audit_subject\".\"result_outcome\" IS NOT NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.auth_account": {
+ "name": "auth_account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_account_id": {
+ "name": "provider_account_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "auth_account_user_id_auth_user_id_fk": {
+ "name": "auth_account_user_id_auth_user_id_fk",
+ "tableFrom": "auth_account",
+ "tableTo": "auth_user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "auth_account_provider_provider_account_id_pk": {
+ "name": "auth_account_provider_provider_account_id_pk",
+ "columns": ["provider", "provider_account_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.auth_judge_session": {
+ "name": "auth_judge_session",
+ "schema": "",
+ "columns": {
+ "session_token": {
+ "name": "session_token",
+ "type": "varchar(255)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "room_name": {
+ "name": "room_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.auth_permissions": {
+ "name": "auth_permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "auth_permissions_role_id_auth_roles_id_fk": {
+ "name": "auth_permissions_role_id_auth_roles_id_fk",
+ "tableFrom": "auth_permissions",
+ "tableTo": "auth_roles",
+ "columnsFrom": ["role_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "auth_permissions_user_id_auth_user_id_fk": {
+ "name": "auth_permissions_user_id_auth_user_id_fk",
+ "tableFrom": "auth_permissions",
+ "tableTo": "auth_user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.auth_roles": {
+ "name": "auth_roles",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "discord_role_id": {
+ "name": "discord_role_id",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permissions": {
+ "name": "permissions",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_feedback_excluded": {
+ "name": "event_feedback_excluded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "email_audience_enabled": {
+ "name": "email_audience_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "issue_reminder_channel": {
+ "name": "issue_reminder_channel",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'1459204271655489567'"
+ },
+ "issue_reminders_enabled": {
+ "name": "issue_reminders_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "team_hexcode_color": {
+ "name": "team_hexcode_color",
+ "type": "varchar(7)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "auth_roles_discordRoleId_unique": {
+ "name": "auth_roles_discordRoleId_unique",
+ "nullsNotDistinct": false,
+ "columns": ["discord_role_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.auth_session": {
+ "name": "auth_session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "session_token": {
+ "name": "session_token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "auth_session_user_id_auth_user_id_fk": {
+ "name": "auth_session_user_id_auth_user_id_fk",
+ "tableFrom": "auth_session",
+ "tableTo": "auth_user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.auth_user": {
+ "name": "auth_user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "discord_user_id": {
+ "name": "discord_user_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "image": {
+ "name": "image",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.auth_verification": {
+ "name": "auth_verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_club_team": {
+ "name": "knight_hacks_club_team",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "heading": {
+ "name": "heading",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_club_team_display_order_unique": {
+ "name": "knight_hacks_club_team_display_order_unique",
+ "columns": [
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_club_team_kind_unique": {
+ "name": "knight_hacks_club_team_kind_unique",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knight_hacks_club_team\".\"kind\" <> 'team'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_club_team_slug_unique": {
+ "name": "knight_hacks_club_team_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": ["slug"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_club_team_kind_check": {
+ "name": "knight_hacks_club_team_kind_check",
+ "value": "\"knight_hacks_club_team\".\"kind\" IN ('executive', 'director', 'team')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_club_team_role": {
+ "name": "knight_hacks_club_team_role",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rank": {
+ "name": "rank",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "team_id": {
+ "name": "team_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "roster_label": {
+ "name": "roster_label",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "callout_label": {
+ "name": "callout_label",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_club_team_role_role_id_auth_roles_id_fk": {
+ "name": "knight_hacks_club_team_role_role_id_auth_roles_id_fk",
+ "tableFrom": "knight_hacks_club_team_role",
+ "tableTo": "auth_roles",
+ "columnsFrom": ["role_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_club_team_role_team_id_knight_hacks_club_team_id_fk": {
+ "name": "knight_hacks_club_team_role_team_id_knight_hacks_club_team_id_fk",
+ "tableFrom": "knight_hacks_club_team_role",
+ "tableTo": "knight_hacks_club_team",
+ "columnsFrom": ["team_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_club_team_role_roleId_unique": {
+ "name": "knight_hacks_club_team_role_roleId_unique",
+ "nullsNotDistinct": false,
+ "columns": ["role_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_club_team_role_kind_check": {
+ "name": "knight_hacks_club_team_role_kind_check",
+ "value": "\"knight_hacks_club_team_role\".\"kind\" IN ('executive', 'director', 'team')"
+ },
+ "knight_hacks_club_team_role_team_check": {
+ "name": "knight_hacks_club_team_role_team_check",
+ "value": "\"knight_hacks_club_team_role\".\"kind\" <> 'team' OR \"knight_hacks_club_team_role\".\"team_id\" IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_discord_config": {
+ "name": "knight_hacks_discord_config",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "production_id": {
+ "name": "production_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "development_id": {
+ "name": "development_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_discord_config_key_unique": {
+ "name": "knight_hacks_discord_config_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_discord_config_kind_check": {
+ "name": "knight_hacks_discord_config_kind_check",
+ "value": "\"knight_hacks_discord_config\".\"kind\" IN ('channel', 'guild', 'role')"
+ },
+ "knight_hacks_discord_config_production_id_check": {
+ "name": "knight_hacks_discord_config_production_id_check",
+ "value": "\"knight_hacks_discord_config\".\"production_id\" ~ '^[0-9]{17,20}$'"
+ },
+ "knight_hacks_discord_config_development_id_check": {
+ "name": "knight_hacks_discord_config_development_id_check",
+ "value": "\"knight_hacks_discord_config\".\"development_id\" IS NULL OR \"knight_hacks_discord_config\".\"development_id\" ~ '^[0-9]{17,20}$'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.discord_archive_channel": {
+ "name": "discord_archive_channel",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(20)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "guild_id": {
+ "name": "guild_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "topic": {
+ "name": "topic",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_thread": {
+ "name": "is_thread",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_private_thread": {
+ "name": "is_private_thread",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "archived": {
+ "name": "archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "locked": {
+ "name": "locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "discovered_at": {
+ "name": "discovered_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "discord_updated_at": {
+ "name": "discord_updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "discord_archive_channel_guild_type_idx": {
+ "name": "discord_archive_channel_guild_type_idx",
+ "columns": [
+ {
+ "expression": "guild_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "discord_archive_channel_parent_idx": {
+ "name": "discord_archive_channel_parent_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.discord_archive_checkpoint": {
+ "name": "discord_archive_checkpoint",
+ "schema": "",
+ "columns": {
+ "channel_id": {
+ "name": "channel_id",
+ "type": "varchar(20)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "guild_id": {
+ "name": "guild_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oldest_message_id": {
+ "name": "oldest_message_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "newest_message_id": {
+ "name": "newest_message_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "backfill_before_message_id": {
+ "name": "backfill_before_message_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "backfill_status": {
+ "name": "backfill_status",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "backfill_completed_at": {
+ "name": "backfill_completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_backfill_at": {
+ "name": "last_backfill_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_discovered_at": {
+ "name": "last_discovered_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_reconciled_at": {
+ "name": "last_reconciled_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processed_message_count": {
+ "name": "processed_message_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "retry_count": {
+ "name": "retry_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_error_code": {
+ "name": "last_error_code",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error_message": {
+ "name": "last_error_message",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "discord_archive_checkpoint_guild_status_updated_idx": {
+ "name": "discord_archive_checkpoint_guild_status_updated_idx",
+ "columns": [
+ {
+ "expression": "guild_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "backfill_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "discord_archive_checkpoint_channel_id_discord_archive_channel_id_fk": {
+ "name": "discord_archive_checkpoint_channel_id_discord_archive_channel_id_fk",
+ "tableFrom": "discord_archive_checkpoint",
+ "tableTo": "discord_archive_channel",
+ "columnsFrom": ["channel_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "discord_archive_checkpoint_status_check": {
+ "name": "discord_archive_checkpoint_status_check",
+ "value": "\"discord_archive_checkpoint\".\"backfill_status\" IN ('pending', 'running', 'complete', 'failed')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.discord_archive_message": {
+ "name": "discord_archive_message",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(20)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "guild_id": {
+ "name": "guild_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_discord_user_id": {
+ "name": "author_discord_user_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author_label": {
+ "name": "author_label",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author_avatar_url": {
+ "name": "author_avatar_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author_is_bot": {
+ "name": "author_is_bot",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "webhook_id": {
+ "name": "webhook_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "application_id": {
+ "name": "application_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "message_type": {
+ "name": "message_type",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "edited_at": {
+ "name": "edited_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reply_to_message_id": {
+ "name": "reply_to_message_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pinned": {
+ "name": "pinned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "flags": {
+ "name": "flags",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "mentions": {
+ "name": "mentions",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"everyone\":false,\"roleIds\":[],\"userIds\":[]}'::jsonb"
+ },
+ "embeds": {
+ "name": "embeds",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "attachments": {
+ "name": "attachments",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "components": {
+ "name": "components",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "stickers": {
+ "name": "stickers",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "poll": {
+ "name": "poll",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ingested_at": {
+ "name": "ingested_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_observed_at": {
+ "name": "last_observed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "discord_archive_message_author_created_idx": {
+ "name": "discord_archive_message_author_created_idx",
+ "columns": [
+ {
+ "expression": "author_discord_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "discord_archive_message_channel_created_idx": {
+ "name": "discord_archive_message_channel_created_idx",
+ "columns": [
+ {
+ "expression": "channel_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "discord_archive_message_guild_created_idx": {
+ "name": "discord_archive_message_guild_created_idx",
+ "columns": [
+ {
+ "expression": "guild_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "discord_archive_message_non_deleted_created_idx": {
+ "name": "discord_archive_message_non_deleted_created_idx",
+ "columns": [
+ {
+ "expression": "guild_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"discord_archive_message\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "discord_archive_message_channel_id_discord_archive_channel_id_fk": {
+ "name": "discord_archive_message_channel_id_discord_archive_channel_id_fk",
+ "tableFrom": "discord_archive_message",
+ "tableTo": "discord_archive_channel",
+ "columnsFrom": ["channel_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.discord_archive_state": {
+ "name": "discord_archive_state",
+ "schema": "",
+ "columns": {
+ "guild_id": {
+ "name": "guild_id",
+ "type": "varchar(20)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "last_gateway_event_at": {
+ "name": "last_gateway_event_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_live_write_at": {
+ "name": "last_live_write_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_discovery_started_at": {
+ "name": "last_discovery_started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_discovery_completed_at": {
+ "name": "last_discovery_completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_reconciliation_started_at": {
+ "name": "last_reconciliation_started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_reconciliation_completed_at": {
+ "name": "last_reconciliation_completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_backfill_progress_at": {
+ "name": "last_backfill_progress_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_owner": {
+ "name": "lease_owner",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_expires_at": {
+ "name": "lease_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error_code": {
+ "name": "last_error_code",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error_message": {
+ "name": "last_error_message",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failure_count": {
+ "name": "failure_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "discord_archive_state_status_check": {
+ "name": "discord_archive_state_status_check",
+ "value": "\"discord_archive_state\".\"status\" IN ('disabled', 'idle', 'healthy', 'degraded', 'failed')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_alumni_bulletin_post": {
+ "name": "knight_hacks_alumni_bulletin_post",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_object_name": {
+ "name": "image_object_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_alt": {
+ "name": "image_alt",
+ "type": "varchar(240)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cta_label": {
+ "name": "cta_label",
+ "type": "varchar(80)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_url": {
+ "name": "external_url",
+ "type": "varchar(2048)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "form_id": {
+ "name": "form_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "alumni_bulletin_state",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "publish_at": {
+ "name": "publish_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_by_user_id": {
+ "name": "updated_by_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_alumni_bulletin_state_order_idx": {
+ "name": "knight_hacks_alumni_bulletin_state_order_idx",
+ "columns": [
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_alumni_bulletin_publication_window_idx": {
+ "name": "knight_hacks_alumni_bulletin_publication_window_idx",
+ "columns": [
+ {
+ "expression": "publish_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_alumni_bulletin_post_form_id_knight_hacks_form_schemas_id_fk": {
+ "name": "knight_hacks_alumni_bulletin_post_form_id_knight_hacks_form_schemas_id_fk",
+ "tableFrom": "knight_hacks_alumni_bulletin_post",
+ "tableTo": "knight_hacks_form_schemas",
+ "columnsFrom": ["form_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_alumni_bulletin_post_created_by_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_alumni_bulletin_post_created_by_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_alumni_bulletin_post",
+ "tableTo": "auth_user",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_alumni_bulletin_post_updated_by_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_alumni_bulletin_post_updated_by_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_alumni_bulletin_post",
+ "tableTo": "auth_user",
+ "columnsFrom": ["updated_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "alumni_bulletin_action_exclusive": {
+ "name": "alumni_bulletin_action_exclusive",
+ "value": "NOT (\"knight_hacks_alumni_bulletin_post\".\"external_url\" IS NOT NULL AND \"knight_hacks_alumni_bulletin_post\".\"form_id\" IS NOT NULL)"
+ },
+ "alumni_bulletin_action_pair": {
+ "name": "alumni_bulletin_action_pair",
+ "value": "((\"knight_hacks_alumni_bulletin_post\".\"external_url\" IS NULL AND \"knight_hacks_alumni_bulletin_post\".\"form_id\" IS NULL) OR \"knight_hacks_alumni_bulletin_post\".\"cta_label\" IS NOT NULL)"
+ },
+ "alumni_bulletin_display_order_nonnegative": {
+ "name": "alumni_bulletin_display_order_nonnegative",
+ "value": "\"knight_hacks_alumni_bulletin_post\".\"display_order\" >= 0"
+ },
+ "alumni_bulletin_image_alt_pair": {
+ "name": "alumni_bulletin_image_alt_pair",
+ "value": "((\"knight_hacks_alumni_bulletin_post\".\"image_object_name\" IS NULL AND \"knight_hacks_alumni_bulletin_post\".\"image_alt\" IS NULL) OR (\"knight_hacks_alumni_bulletin_post\".\"image_object_name\" IS NOT NULL AND \"knight_hacks_alumni_bulletin_post\".\"image_alt\" IS NOT NULL))"
+ },
+ "alumni_bulletin_schedule_order": {
+ "name": "alumni_bulletin_schedule_order",
+ "value": "\"knight_hacks_alumni_bulletin_post\".\"expires_at\" IS NULL OR \"knight_hacks_alumni_bulletin_post\".\"publish_at\" IS NULL OR \"knight_hacks_alumni_bulletin_post\".\"expires_at\" > \"knight_hacks_alumni_bulletin_post\".\"publish_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_challenges": {
+ "name": "knight_hacks_challenges",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sponsor": {
+ "name": "sponsor",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_challenges_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_challenges_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_challenges",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_challenges_title_hackathonId_unique": {
+ "name": "knight_hacks_challenges_title_hackathonId_unique",
+ "nullsNotDistinct": false,
+ "columns": ["title", "hackathon_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_company": {
+ "name": "knight_hacks_company",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_display_name": {
+ "name": "normalized_display_name",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "legal_name": {
+ "name": "legal_name",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "domain": {
+ "name": "domain",
+ "type": "varchar(253)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "logo_object_name": {
+ "name": "logo_object_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "aliases": {
+ "name": "aliases",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "review_state": {
+ "name": "review_state",
+ "type": "company_review_state",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "merged_into_company_id": {
+ "name": "merged_into_company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_company_review_state_idx": {
+ "name": "knight_hacks_company_review_state_idx",
+ "columns": [
+ {
+ "expression": "review_state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_company_domain_idx": {
+ "name": "knight_hacks_company_domain_idx",
+ "columns": [
+ {
+ "expression": "domain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_company_created_by_user_idx": {
+ "name": "knight_hacks_company_created_by_user_idx",
+ "columns": [
+ {
+ "expression": "created_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_company_created_by_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_company_created_by_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_company",
+ "tableTo": "auth_user",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_company_merged_into_fk": {
+ "name": "knight_hacks_company_merged_into_fk",
+ "tableFrom": "knight_hacks_company",
+ "tableTo": "knight_hacks_company",
+ "columnsFrom": ["merged_into_company_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_company_normalized_display_name_unique": {
+ "name": "knight_hacks_company_normalized_display_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["normalized_display_name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_company_merged_state_consistency": {
+ "name": "knight_hacks_company_merged_state_consistency",
+ "value": "(\"knight_hacks_company\".\"review_state\" = 'merged') = (\"knight_hacks_company\".\"merged_into_company_id\" IS NOT NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_dues_checkout_reservation": {
+ "name": "knight_hacks_dues_checkout_reservation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "member_id": {
+ "name": "member_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "year": {
+ "name": "year",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attempt_number": {
+ "name": "attempt_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "stripe_payment_intent_id": {
+ "name": "stripe_payment_intent_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_dues_checkout_reservation_member_id_knight_hacks_member_id_fk": {
+ "name": "knight_hacks_dues_checkout_reservation_member_id_knight_hacks_member_id_fk",
+ "tableFrom": "knight_hacks_dues_checkout_reservation",
+ "tableTo": "knight_hacks_member",
+ "columnsFrom": ["member_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_dues_checkout_reservation_memberId_year_unique": {
+ "name": "knight_hacks_dues_checkout_reservation_memberId_year_unique",
+ "nullsNotDistinct": false,
+ "columns": ["member_id", "year"]
+ },
+ "knight_hacks_dues_checkout_reservation_stripe_payment_intent_id_unique": {
+ "name": "knight_hacks_dues_checkout_reservation_stripe_payment_intent_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["stripe_payment_intent_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_dues_configuration": {
+ "name": "knight_hacks_dues_configuration",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(32)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "payments_enabled": {
+ "name": "payments_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_dues_configuration_singleton_check": {
+ "name": "knight_hacks_dues_configuration_singleton_check",
+ "value": "\"knight_hacks_dues_configuration\".\"id\" = 'global'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_dues_entitlement": {
+ "name": "knight_hacks_dues_entitlement",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "member_id": {
+ "name": "member_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "year": {
+ "name": "year",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "source_payment_id": {
+ "name": "source_payment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_dues_entitlement_member_id_knight_hacks_member_id_fk": {
+ "name": "knight_hacks_dues_entitlement_member_id_knight_hacks_member_id_fk",
+ "tableFrom": "knight_hacks_dues_entitlement",
+ "tableTo": "knight_hacks_member",
+ "columnsFrom": ["member_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_dues_entitlement_source_payment_id_knight_hacks_dues_payment_id_fk": {
+ "name": "knight_hacks_dues_entitlement_source_payment_id_knight_hacks_dues_payment_id_fk",
+ "tableFrom": "knight_hacks_dues_entitlement",
+ "tableTo": "knight_hacks_dues_payment",
+ "columnsFrom": ["source_payment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_dues_entitlement_memberId_year_unique": {
+ "name": "knight_hacks_dues_entitlement_memberId_year_unique",
+ "nullsNotDistinct": false,
+ "columns": ["member_id", "year"]
+ },
+ "knight_hacks_dues_entitlement_source_payment_id_unique": {
+ "name": "knight_hacks_dues_entitlement_source_payment_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["source_payment_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_dues_payment": {
+ "name": "knight_hacks_dues_payment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "member_id": {
+ "name": "member_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "amount": {
+ "name": "amount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payment_date": {
+ "name": "payment_date",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "year": {
+ "name": "year",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_payment_intent_id": {
+ "name": "stripe_payment_intent_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_dues_payment_member_id_knight_hacks_member_id_fk": {
+ "name": "knight_hacks_dues_payment_member_id_knight_hacks_member_id_fk",
+ "tableFrom": "knight_hacks_dues_payment",
+ "tableTo": "knight_hacks_member",
+ "columnsFrom": ["member_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_dues_payment_stripe_payment_intent_id_unique": {
+ "name": "knight_hacks_dues_payment_stripe_payment_intent_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["stripe_payment_intent_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_send": {
+ "name": "email_send",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "template_revision_id": {
+ "name": "template_revision_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "plain_text_source": {
+ "name": "plain_text_source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "compiled_html": {
+ "name": "compiled_html",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "compiled_text": {
+ "name": "compiled_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_hash": {
+ "name": "content_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "audience_definition": {
+ "name": "audience_definition",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "audience_version": {
+ "name": "audience_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "audience_hash": {
+ "name": "audience_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "preview_version": {
+ "name": "preview_version",
+ "type": "varchar(80)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "preview_expires_at": {
+ "name": "preview_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "raw_match_count": {
+ "name": "raw_match_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "duplicate_count": {
+ "name": "duplicate_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "excluded_invalid_count": {
+ "name": "excluded_invalid_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "excluded_suppressed_count": {
+ "name": "excluded_suppressed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "excluded_missing_field_count": {
+ "name": "excluded_missing_field_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "excluded_manual_count": {
+ "name": "excluded_manual_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "final_recipient_count": {
+ "name": "final_recipient_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "scheduled_for": {
+ "name": "scheduled_for",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "listmonk_list_id": {
+ "name": "listmonk_list_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "listmonk_campaign_id": {
+ "name": "listmonk_campaign_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_tag": {
+ "name": "provider_tag",
+ "type": "varchar(80)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_may_have_started": {
+ "name": "provider_may_have_started",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "provider_sent_count": {
+ "name": "provider_sent_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "provider_bounce_count": {
+ "name": "provider_bounce_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "retry_attempt_count": {
+ "name": "retry_attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "retry_lease_expires_at": {
+ "name": "retry_lease_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_retry_at": {
+ "name": "next_retry_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "safe_error": {
+ "name": "safe_error",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "confirmed_at": {
+ "name": "confirmed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancelled_at": {
+ "name": "cancelled_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancelled_by": {
+ "name": "cancelled_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "terminal_at": {
+ "name": "terminal_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_send_status_scheduled_for_idx": {
+ "name": "email_send_status_scheduled_for_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "scheduled_for",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_send_retry_idx": {
+ "name": "email_send_retry_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_retry_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "retry_lease_expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_send_template_revision_id_email_template_revision_id_fk": {
+ "name": "email_send_template_revision_id_email_template_revision_id_fk",
+ "tableFrom": "email_send",
+ "tableTo": "email_template_revision",
+ "columnsFrom": ["template_revision_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "email_send_created_by_auth_user_id_fk": {
+ "name": "email_send_created_by_auth_user_id_fk",
+ "tableFrom": "email_send",
+ "tableTo": "auth_user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "email_send_cancelled_by_auth_user_id_fk": {
+ "name": "email_send_cancelled_by_auth_user_id_fk",
+ "tableFrom": "email_send",
+ "tableTo": "auth_user",
+ "columnsFrom": ["cancelled_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_send_provider_tag_unique": {
+ "name": "email_send_provider_tag_unique",
+ "nullsNotDistinct": false,
+ "columns": ["provider_tag"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_send_event": {
+ "name": "email_send_event",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "send_id": {
+ "name": "send_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "from_status": {
+ "name": "from_status",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "to_status": {
+ "name": "to_status",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_send_event_send_created_idx": {
+ "name": "email_send_event_send_created_idx",
+ "columns": [
+ {
+ "expression": "send_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_send_event_send_id_email_send_id_fk": {
+ "name": "email_send_event_send_id_email_send_id_fk",
+ "tableFrom": "email_send_event",
+ "tableTo": "email_send",
+ "columnsFrom": ["send_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "email_send_event_actor_id_auth_user_id_fk": {
+ "name": "email_send_event_actor_id_auth_user_id_fk",
+ "tableFrom": "email_send_event",
+ "tableTo": "auth_user",
+ "columnsFrom": ["actor_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_send_recipient": {
+ "name": "email_send_recipient",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "send_id": {
+ "name": "send_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(320)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_email": {
+ "name": "normalized_email",
+ "type": "varchar(320)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attributes": {
+ "name": "attributes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "match_reasons": {
+ "name": "match_reasons",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "exclusion_reason": {
+ "name": "exclusion_reason",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "listmonk_subscriber_id": {
+ "name": "listmonk_subscriber_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_send_recipient_send_id_idx": {
+ "name": "email_send_recipient_send_id_idx",
+ "columns": [
+ {
+ "expression": "send_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "exclusion_reason",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_send_recipient_send_id_email_send_id_fk": {
+ "name": "email_send_recipient_send_id_email_send_id_fk",
+ "tableFrom": "email_send_recipient",
+ "tableTo": "email_send",
+ "columnsFrom": ["send_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_send_recipient_send_id_normalized_email_unique": {
+ "name": "email_send_recipient_send_id_normalized_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["send_id", "normalized_email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_template": {
+ "name": "email_template",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_name": {
+ "name": "normalized_name",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'club'"
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_template_archived_updated_idx": {
+ "name": "email_template_archived_updated_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_template_created_by_auth_user_id_fk": {
+ "name": "email_template_created_by_auth_user_id_fk",
+ "tableFrom": "email_template",
+ "tableTo": "auth_user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "email_template_updated_by_auth_user_id_fk": {
+ "name": "email_template_updated_by_auth_user_id_fk",
+ "tableFrom": "email_template",
+ "tableTo": "auth_user",
+ "columnsFrom": ["updated_by"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_template_normalized_name_unique": {
+ "name": "email_template_normalized_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["normalized_name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_template_revision": {
+ "name": "email_template_revision",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "visual_document": {
+ "name": "visual_document",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "compiled_html": {
+ "name": "compiled_html",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "compiled_text": {
+ "name": "compiled_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "personalization_contract": {
+ "name": "personalization_contract",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "checksum": {
+ "name": "checksum",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_template_revision_template_state_idx": {
+ "name": "email_template_revision_template_state_idx",
+ "columns": [
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "version",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_template_revision_template_id_email_template_id_fk": {
+ "name": "email_template_revision_template_id_email_template_id_fk",
+ "tableFrom": "email_template_revision",
+ "tableTo": "email_template",
+ "columnsFrom": ["template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "email_template_revision_created_by_auth_user_id_fk": {
+ "name": "email_template_revision_created_by_auth_user_id_fk",
+ "tableFrom": "email_template_revision",
+ "tableTo": "auth_user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_template_revision_template_version_unique": {
+ "name": "email_template_revision_template_version_unique",
+ "nullsNotDistinct": false,
+ "columns": ["template_id", "version"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_employment": {
+ "name": "knight_hacks_employment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "member_id": {
+ "name": "member_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "experience_type": {
+ "name": "experience_type",
+ "type": "employment_experience_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "employment_state",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "start_month": {
+ "name": "start_month",
+ "type": "varchar(7)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "end_month": {
+ "name": "end_month",
+ "type": "varchar(7)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "city_key": {
+ "name": "city_key",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "guild_visible": {
+ "name": "guild_visible",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_employment_member_idx": {
+ "name": "knight_hacks_employment_member_idx",
+ "columns": [
+ {
+ "expression": "member_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_employment_company_idx": {
+ "name": "knight_hacks_employment_company_idx",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_employment_company_state_idx": {
+ "name": "knight_hacks_employment_company_state_idx",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_employment_member_id_knight_hacks_member_id_fk": {
+ "name": "knight_hacks_employment_member_id_knight_hacks_member_id_fk",
+ "tableFrom": "knight_hacks_employment",
+ "tableTo": "knight_hacks_member",
+ "columnsFrom": ["member_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_employment_company_id_knight_hacks_company_id_fk": {
+ "name": "knight_hacks_employment_company_id_knight_hacks_company_id_fk",
+ "tableFrom": "knight_hacks_employment",
+ "tableTo": "knight_hacks_company",
+ "columnsFrom": ["company_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_employment_current_has_no_end": {
+ "name": "knight_hacks_employment_current_has_no_end",
+ "value": "\"knight_hacks_employment\".\"state\" <> 'current' OR \"knight_hacks_employment\".\"end_month\" IS NULL"
+ },
+ "knight_hacks_employment_start_month_shape": {
+ "name": "knight_hacks_employment_start_month_shape",
+ "value": "\"knight_hacks_employment\".\"start_month\" IS NULL OR \"knight_hacks_employment\".\"start_month\" ~ '^[0-9]{4}-(0[1-9]|1[0-2])$'"
+ },
+ "knight_hacks_employment_end_month_shape": {
+ "name": "knight_hacks_employment_end_month_shape",
+ "value": "\"knight_hacks_employment\".\"end_month\" IS NULL OR \"knight_hacks_employment\".\"end_month\" ~ '^[0-9]{4}-(0[1-9]|1[0-2])$'"
+ },
+ "knight_hacks_employment_date_order": {
+ "name": "knight_hacks_employment_date_order",
+ "value": "\"knight_hacks_employment\".\"start_month\" IS NULL OR \"knight_hacks_employment\".\"end_month\" IS NULL OR \"knight_hacks_employment\".\"end_month\" >= \"knight_hacks_employment\".\"start_month\""
+ },
+ "knight_hacks_employment_city_key_shape": {
+ "name": "knight_hacks_employment_city_key_shape",
+ "value": "\"knight_hacks_employment\".\"city_key\" IS NULL OR \"knight_hacks_employment\".\"city_key\" ~ '^[0-9]{2}-[0-9]{5}$'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_event": {
+ "name": "knight_hacks_event",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "discord_id": {
+ "name": "discord_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "google_id": {
+ "name": "google_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag": {
+ "name": "tag",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_color": {
+ "name": "tag_color",
+ "type": "varchar(7)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#0D9488'"
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "start_datetime": {
+ "name": "start_datetime",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_datetime": {
+ "name": "end_datetime",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "location": {
+ "name": "location",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dues_paying": {
+ "name": "dues_paying",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_operations_calendar": {
+ "name": "is_operations_calendar",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "roles": {
+ "name": "roles",
+ "type": "varchar(255)[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "points": {
+ "name": "points",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "purpose": {
+ "name": "purpose",
+ "type": "event_purpose",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'event'"
+ },
+ "discord_channel_id": {
+ "name": "discord_channel_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "legacy": {
+ "name": "legacy",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "discord_sync_state": {
+ "name": "discord_sync_state",
+ "type": "event_sync_state",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'pending'"
+ },
+ "google_sync_state": {
+ "name": "google_sync_state",
+ "type": "event_sync_state",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'pending'"
+ },
+ "discord_last_error": {
+ "name": "discord_last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "google_last_error": {
+ "name": "google_last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deletion_intent_at": {
+ "name": "deletion_intent_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "creation_key": {
+ "name": "creation_key",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "creation_payload_hash": {
+ "name": "creation_payload_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_revision": {
+ "name": "sync_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "sync_lease_token": {
+ "name": "sync_lease_token",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_lease_expires_at": {
+ "name": "sync_lease_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_lease_revision": {
+ "name": "sync_lease_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discord_outbound_attempt_token": {
+ "name": "discord_outbound_attempt_token",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discord_outbound_attempted_at": {
+ "name": "discord_outbound_attempted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discord_outbound_attempt_revision": {
+ "name": "discord_outbound_attempt_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "google_outbound_attempt_token": {
+ "name": "google_outbound_attempt_token",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "google_outbound_attempted_at": {
+ "name": "google_outbound_attempted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "google_outbound_attempt_revision": {
+ "name": "google_outbound_attempt_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discord_applied_revision": {
+ "name": "discord_applied_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discord_applied_entity_type": {
+ "name": "discord_applied_entity_type",
+ "type": "event_discord_entity_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discord_applied_channel_id": {
+ "name": "discord_applied_channel_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "google_applied_revision": {
+ "name": "google_applied_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "google_applied_destination": {
+ "name": "google_applied_destination",
+ "type": "event_google_destination",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "google_applied_calendar_id": {
+ "name": "google_applied_calendar_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "visibility_revision": {
+ "name": "visibility_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "visibility_dues_paying": {
+ "name": "visibility_dues_paying",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "visibility_roles": {
+ "name": "visibility_roles",
+ "type": "varchar(255)[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "visibility_internal": {
+ "name": "visibility_internal",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discord_no_projection_acknowledged_at": {
+ "name": "discord_no_projection_acknowledged_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discord_no_projection_acknowledged_by": {
+ "name": "discord_no_projection_acknowledged_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "knight_hacks_event_start_datetime_idx": {
+ "name": "knight_hacks_event_start_datetime_idx",
+ "columns": [
+ {
+ "expression": "start_datetime",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_event_hackathon_id_idx": {
+ "name": "knight_hacks_event_hackathon_id_idx",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_event_one_primary_per_hackathon": {
+ "name": "knight_hacks_event_one_primary_per_hackathon",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knight_hacks_event\".\"purpose\" = 'primary_check_in'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_event_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_event_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_event",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_event_discord_no_projection_acknowledged_by_auth_user_id_fk": {
+ "name": "knight_hacks_event_discord_no_projection_acknowledged_by_auth_user_id_fk",
+ "tableFrom": "knight_hacks_event",
+ "tableTo": "auth_user",
+ "columnsFrom": ["discord_no_projection_acknowledged_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_event_creationKey_unique": {
+ "name": "knight_hacks_event_creationKey_unique",
+ "nullsNotDistinct": false,
+ "columns": ["creation_key"]
+ },
+ "knight_hacks_event_id_hackathon_unique": {
+ "name": "knight_hacks_event_id_hackathon_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "hackathon_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_event_primary_requires_hackathon_check": {
+ "name": "knight_hacks_event_primary_requires_hackathon_check",
+ "value": "\"knight_hacks_event\".\"purpose\" <> 'primary_check_in' OR \"knight_hacks_event\".\"hackathon_id\" IS NOT NULL"
+ },
+ "knight_hacks_event_tag_color_check": {
+ "name": "knight_hacks_event_tag_color_check",
+ "value": "\"knight_hacks_event\".\"tag_color\" ~ '^#[0-9A-Fa-f]{6}$'"
+ },
+ "knight_hacks_event_sync_revision_check": {
+ "name": "knight_hacks_event_sync_revision_check",
+ "value": "\"knight_hacks_event\".\"sync_revision\" >= 0"
+ },
+ "knight_hacks_event_new_points_check": {
+ "name": "knight_hacks_event_new_points_check",
+ "value": "\"knight_hacks_event\".\"legacy\" OR (\"knight_hacks_event\".\"points\" IS NOT NULL AND \"knight_hacks_event\".\"points\" >= 0)"
+ },
+ "knight_hacks_event_nonlegacy_sync_states_check": {
+ "name": "knight_hacks_event_nonlegacy_sync_states_check",
+ "value": "\"knight_hacks_event\".\"legacy\" OR (\"knight_hacks_event\".\"discord_sync_state\" IS NOT NULL AND \"knight_hacks_event\".\"google_sync_state\" IS NOT NULL)"
+ },
+ "knight_hacks_event_creation_identity_pair_check": {
+ "name": "knight_hacks_event_creation_identity_pair_check",
+ "value": "(\"knight_hacks_event\".\"creation_key\" IS NULL) = (\"knight_hacks_event\".\"creation_payload_hash\" IS NULL)"
+ },
+ "knight_hacks_event_new_creation_identity_check": {
+ "name": "knight_hacks_event_new_creation_identity_check",
+ "value": "\"knight_hacks_event\".\"legacy\" OR (\"knight_hacks_event\".\"creation_key\" IS NOT NULL AND \"knight_hacks_event\".\"creation_payload_hash\" IS NOT NULL)"
+ },
+ "knight_hacks_event_sync_lease_set_check": {
+ "name": "knight_hacks_event_sync_lease_set_check",
+ "value": "(\"knight_hacks_event\".\"sync_lease_token\" IS NULL AND \"knight_hacks_event\".\"sync_lease_expires_at\" IS NULL AND \"knight_hacks_event\".\"sync_lease_revision\" IS NULL) OR (\"knight_hacks_event\".\"sync_lease_token\" IS NOT NULL AND \"knight_hacks_event\".\"sync_lease_expires_at\" IS NOT NULL AND \"knight_hacks_event\".\"sync_lease_revision\" IS NOT NULL)"
+ },
+ "knight_hacks_event_discord_attempt_set_check": {
+ "name": "knight_hacks_event_discord_attempt_set_check",
+ "value": "(\"knight_hacks_event\".\"discord_outbound_attempt_token\" IS NULL AND \"knight_hacks_event\".\"discord_outbound_attempted_at\" IS NULL AND \"knight_hacks_event\".\"discord_outbound_attempt_revision\" IS NULL) OR (\"knight_hacks_event\".\"discord_outbound_attempt_token\" IS NOT NULL AND \"knight_hacks_event\".\"discord_outbound_attempted_at\" IS NOT NULL AND \"knight_hacks_event\".\"discord_outbound_attempt_revision\" IS NOT NULL)"
+ },
+ "knight_hacks_event_google_attempt_set_check": {
+ "name": "knight_hacks_event_google_attempt_set_check",
+ "value": "(\"knight_hacks_event\".\"google_outbound_attempt_token\" IS NULL AND \"knight_hacks_event\".\"google_outbound_attempted_at\" IS NULL AND \"knight_hacks_event\".\"google_outbound_attempt_revision\" IS NULL) OR (\"knight_hacks_event\".\"google_outbound_attempt_token\" IS NOT NULL AND \"knight_hacks_event\".\"google_outbound_attempted_at\" IS NOT NULL AND \"knight_hacks_event\".\"google_outbound_attempt_revision\" IS NOT NULL)"
+ },
+ "knight_hacks_event_visibility_set_check": {
+ "name": "knight_hacks_event_visibility_set_check",
+ "value": "(\"knight_hacks_event\".\"visibility_revision\" IS NULL AND \"knight_hacks_event\".\"visibility_dues_paying\" IS NULL AND \"knight_hacks_event\".\"visibility_roles\" IS NULL AND \"knight_hacks_event\".\"visibility_internal\" IS NULL) OR (\"knight_hacks_event\".\"visibility_revision\" IS NOT NULL AND \"knight_hacks_event\".\"visibility_dues_paying\" IS NOT NULL AND \"knight_hacks_event\".\"visibility_roles\" IS NOT NULL AND \"knight_hacks_event\".\"visibility_internal\" IS NOT NULL)"
+ },
+ "knight_hacks_event_visibility_revision_check": {
+ "name": "knight_hacks_event_visibility_revision_check",
+ "value": "\"knight_hacks_event\".\"visibility_revision\" IS NULL OR \"knight_hacks_event\".\"visibility_revision\" <= \"knight_hacks_event\".\"sync_revision\""
+ },
+ "knight_hacks_event_discord_applied_revision_check": {
+ "name": "knight_hacks_event_discord_applied_revision_check",
+ "value": "\"knight_hacks_event\".\"discord_applied_revision\" IS NULL OR \"knight_hacks_event\".\"discord_applied_revision\" <= \"knight_hacks_event\".\"sync_revision\""
+ },
+ "knight_hacks_event_google_applied_revision_check": {
+ "name": "knight_hacks_event_google_applied_revision_check",
+ "value": "\"knight_hacks_event\".\"google_applied_revision\" IS NULL OR \"knight_hacks_event\".\"google_applied_revision\" <= \"knight_hacks_event\".\"sync_revision\""
+ },
+ "knight_hacks_event_discord_synced_state_check": {
+ "name": "knight_hacks_event_discord_synced_state_check",
+ "value": "\"knight_hacks_event\".\"discord_sync_state\" IS DISTINCT FROM 'synced' OR (\"knight_hacks_event\".\"discord_id\" IS NOT NULL AND \"knight_hacks_event\".\"discord_applied_revision\" IS NOT NULL AND \"knight_hacks_event\".\"discord_applied_revision\" = \"knight_hacks_event\".\"sync_revision\" AND \"knight_hacks_event\".\"discord_applied_entity_type\" IS NOT NULL AND ((\"knight_hacks_event\".\"discord_applied_entity_type\" = 'external' AND \"knight_hacks_event\".\"discord_applied_channel_id\" IS NULL) OR (\"knight_hacks_event\".\"discord_applied_entity_type\" IN ('voice', 'stage') AND \"knight_hacks_event\".\"discord_applied_channel_id\" IS NOT NULL)))"
+ },
+ "knight_hacks_event_google_synced_state_check": {
+ "name": "knight_hacks_event_google_synced_state_check",
+ "value": "\"knight_hacks_event\".\"google_sync_state\" IS DISTINCT FROM 'synced' OR (\"knight_hacks_event\".\"google_id\" IS NOT NULL AND \"knight_hacks_event\".\"google_applied_revision\" IS NOT NULL AND \"knight_hacks_event\".\"google_applied_revision\" = \"knight_hacks_event\".\"sync_revision\" AND \"knight_hacks_event\".\"google_applied_destination\" IS NOT NULL AND \"knight_hacks_event\".\"google_applied_calendar_id\" IS NOT NULL)"
+ },
+ "knight_hacks_event_published_visibility_check": {
+ "name": "knight_hacks_event_published_visibility_check",
+ "value": "\"knight_hacks_event\".\"published_at\" IS NULL OR (\"knight_hacks_event\".\"visibility_revision\" IS NOT NULL AND \"knight_hacks_event\".\"visibility_dues_paying\" IS NOT NULL AND \"knight_hacks_event\".\"visibility_roles\" IS NOT NULL AND \"knight_hacks_event\".\"visibility_internal\" IS NOT NULL)"
+ },
+ "knight_hacks_event_discord_acknowledgement_time_check": {
+ "name": "knight_hacks_event_discord_acknowledgement_time_check",
+ "value": "\"knight_hacks_event\".\"discord_no_projection_acknowledged_by\" IS NULL OR \"knight_hacks_event\".\"discord_no_projection_acknowledged_at\" IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_event_attendee": {
+ "name": "knight_hacks_event_attendee",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "member_id": {
+ "name": "member_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "checked_in_at": {
+ "name": "checked_in_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "checked_in_by": {
+ "name": "checked_in_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "points_awarded": {
+ "name": "points_awarded",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "points_awarded_estimated": {
+ "name": "points_awarded_estimated",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {
+ "knight_hacks_event_attendee_event_member_idx": {
+ "name": "knight_hacks_event_attendee_event_member_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "member_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_event_attendee_member_event_idx": {
+ "name": "knight_hacks_event_attendee_member_event_idx",
+ "columns": [
+ {
+ "expression": "member_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_event_attendee_member_id_knight_hacks_member_id_fk": {
+ "name": "knight_hacks_event_attendee_member_id_knight_hacks_member_id_fk",
+ "tableFrom": "knight_hacks_event_attendee",
+ "tableTo": "knight_hacks_member",
+ "columnsFrom": ["member_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_event_attendee_event_id_knight_hacks_event_id_fk": {
+ "name": "knight_hacks_event_attendee_event_id_knight_hacks_event_id_fk",
+ "tableFrom": "knight_hacks_event_attendee",
+ "tableTo": "knight_hacks_event",
+ "columnsFrom": ["event_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_event_attendee_checked_in_by_auth_user_id_fk": {
+ "name": "knight_hacks_event_attendee_checked_in_by_auth_user_id_fk",
+ "tableFrom": "knight_hacks_event_attendee",
+ "tableTo": "auth_user",
+ "columnsFrom": ["checked_in_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_event_feedback": {
+ "name": "knight_hacks_event_feedback",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "member_id": {
+ "name": "member_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "overall_event_rating": {
+ "name": "overall_event_rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fun_rating": {
+ "name": "fun_rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "learned_rating": {
+ "name": "learned_rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "heard_about_us": {
+ "name": "heard_about_us",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "additional_feedback": {
+ "name": "additional_feedback",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "similar_event": {
+ "name": "similar_event",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_event_feedback_member_id_knight_hacks_member_id_fk": {
+ "name": "knight_hacks_event_feedback_member_id_knight_hacks_member_id_fk",
+ "tableFrom": "knight_hacks_event_feedback",
+ "tableTo": "knight_hacks_member",
+ "columnsFrom": ["member_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_event_feedback_event_id_knight_hacks_event_id_fk": {
+ "name": "knight_hacks_event_feedback_event_id_knight_hacks_event_id_fk",
+ "tableFrom": "knight_hacks_event_feedback",
+ "tableTo": "knight_hacks_event",
+ "columnsFrom": ["event_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_event_feedback_config": {
+ "name": "knight_hacks_event_feedback_config",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "form_id": {
+ "name": "form_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "closes_at": {
+ "name": "closes_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reward_points": {
+ "name": "reward_points",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 5
+ },
+ "template_revision": {
+ "name": "template_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "custom_questions": {
+ "name": "custom_questions",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_event_feedback_config_event_id_knight_hacks_event_id_fk": {
+ "name": "knight_hacks_event_feedback_config_event_id_knight_hacks_event_id_fk",
+ "tableFrom": "knight_hacks_event_feedback_config",
+ "tableTo": "knight_hacks_event",
+ "columnsFrom": ["event_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_event_feedback_config_form_id_knight_hacks_form_schemas_id_fk": {
+ "name": "knight_hacks_event_feedback_config_form_id_knight_hacks_form_schemas_id_fk",
+ "tableFrom": "knight_hacks_event_feedback_config",
+ "tableTo": "knight_hacks_form_schemas",
+ "columnsFrom": ["form_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_event_feedback_config_eventId_unique": {
+ "name": "knight_hacks_event_feedback_config_eventId_unique",
+ "nullsNotDistinct": false,
+ "columns": ["event_id"]
+ },
+ "knight_hacks_event_feedback_config_formId_unique": {
+ "name": "knight_hacks_event_feedback_config_formId_unique",
+ "nullsNotDistinct": false,
+ "columns": ["form_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_event_feedback_config_reward_check": {
+ "name": "knight_hacks_event_feedback_config_reward_check",
+ "value": "\"knight_hacks_event_feedback_config\".\"reward_points\" = 5"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_event_feedback_reward": {
+ "name": "knight_hacks_event_feedback_reward",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "member_id": {
+ "name": "member_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "response_id": {
+ "name": "response_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "points_awarded": {
+ "name": "points_awarded",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 5
+ },
+ "awarded_at": {
+ "name": "awarded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_event_feedback_reward_event_id_knight_hacks_event_id_fk": {
+ "name": "knight_hacks_event_feedback_reward_event_id_knight_hacks_event_id_fk",
+ "tableFrom": "knight_hacks_event_feedback_reward",
+ "tableTo": "knight_hacks_event",
+ "columnsFrom": ["event_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_event_feedback_reward_member_id_knight_hacks_member_id_fk": {
+ "name": "knight_hacks_event_feedback_reward_member_id_knight_hacks_member_id_fk",
+ "tableFrom": "knight_hacks_event_feedback_reward",
+ "tableTo": "knight_hacks_member",
+ "columnsFrom": ["member_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_event_feedback_reward_response_id_knight_hacks_form_response_id_fk": {
+ "name": "knight_hacks_event_feedback_reward_response_id_knight_hacks_form_response_id_fk",
+ "tableFrom": "knight_hacks_event_feedback_reward",
+ "tableTo": "knight_hacks_form_response",
+ "columnsFrom": ["response_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_event_feedback_reward_event_member_unique": {
+ "name": "knight_hacks_event_feedback_reward_event_member_unique",
+ "nullsNotDistinct": false,
+ "columns": ["event_id", "member_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_event_feedback_reward_points_check": {
+ "name": "knight_hacks_event_feedback_reward_points_check",
+ "value": "\"knight_hacks_event_feedback_reward\".\"points_awarded\" = 5"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_event_publication_work": {
+ "name": "knight_hacks_event_publication_work",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "publication_id": {
+ "name": "publication_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "hackathon_event_publication_provider",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_enabled": {
+ "name": "target_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_revision": {
+ "name": "event_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "publication_revision": {
+ "name": "publication_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "event_publication_work_state",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "last_attempt_at": {
+ "name": "last_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_token": {
+ "name": "lease_token",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_expires_at": {
+ "name": "lease_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_event_publication_work_due_idx": {
+ "name": "knight_hacks_event_publication_work_due_idx",
+ "columns": [
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_event_publication_work_health_idx": {
+ "name": "knight_hacks_event_publication_work_health_idx",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_event_publication_work_lease_expiry_idx": {
+ "name": "knight_hacks_event_publication_work_lease_expiry_idx",
+ "columns": [
+ {
+ "expression": "lease_expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_event_publication_work_scoped_publication_fk": {
+ "name": "knight_hacks_event_publication_work_scoped_publication_fk",
+ "tableFrom": "knight_hacks_event_publication_work",
+ "tableTo": "knight_hacks_hackathon_event_publication",
+ "columnsFrom": ["publication_id", "hackathon_id", "provider"],
+ "columnsTo": ["id", "hackathon_id", "provider"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_event_publication_work_scoped_event_fk": {
+ "name": "knight_hacks_event_publication_work_scoped_event_fk",
+ "tableFrom": "knight_hacks_event_publication_work",
+ "tableTo": "knight_hacks_event",
+ "columnsFrom": ["event_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_event_publication_work_event_provider_unique": {
+ "name": "knight_hacks_event_publication_work_event_provider_unique",
+ "nullsNotDistinct": false,
+ "columns": ["event_id", "provider"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_event_publication_work_attempt_count_check": {
+ "name": "knight_hacks_event_publication_work_attempt_count_check",
+ "value": "\"knight_hacks_event_publication_work\".\"attempt_count\" >= 0"
+ },
+ "knight_hacks_event_publication_work_revisions_check": {
+ "name": "knight_hacks_event_publication_work_revisions_check",
+ "value": "\"knight_hacks_event_publication_work\".\"event_revision\" >= 1 AND \"knight_hacks_event_publication_work\".\"publication_revision\" >= 1"
+ },
+ "knight_hacks_event_publication_work_lease_pair_check": {
+ "name": "knight_hacks_event_publication_work_lease_pair_check",
+ "value": "(\"knight_hacks_event_publication_work\".\"lease_token\" IS NULL) = (\"knight_hacks_event_publication_work\".\"lease_expires_at\" IS NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_event_tag": {
+ "name": "knight_hacks_event_tag",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_name": {
+ "name": "normalized_name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_points": {
+ "name": "default_points",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(7)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_event_tag_club_normalized_name_unique": {
+ "name": "knight_hacks_event_tag_club_normalized_name_unique",
+ "columns": [
+ {
+ "expression": "normalized_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knight_hacks_event_tag\".\"hackathon_id\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_event_tag_hackathon_normalized_name_unique": {
+ "name": "knight_hacks_event_tag_hackathon_normalized_name_unique",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knight_hacks_event_tag\".\"hackathon_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_event_tag_hackathon_idx": {
+ "name": "knight_hacks_event_tag_hackathon_idx",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_event_tag_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_event_tag_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_event_tag",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_event_tag_default_points_check": {
+ "name": "knight_hacks_event_tag_default_points_check",
+ "value": "\"knight_hacks_event_tag\".\"default_points\" >= 0"
+ },
+ "knight_hacks_event_tag_color_check": {
+ "name": "knight_hacks_event_tag_color_check",
+ "value": "\"knight_hacks_event_tag\".\"color\" ~ '^#[0-9A-Fa-f]{6}$'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_form_attachment": {
+ "name": "knight_hacks_form_attachment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "form_id": {
+ "name": "form_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "response_id": {
+ "name": "response_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner_user_id": {
+ "name": "owner_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "object_name": {
+ "name": "object_name",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "purpose": {
+ "name": "purpose",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'response'"
+ },
+ "finalized_at": {
+ "name": "finalized_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_form_attachment_form_idx": {
+ "name": "knight_hacks_form_attachment_form_idx",
+ "columns": [
+ {
+ "expression": "form_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_form_attachment_response_idx": {
+ "name": "knight_hacks_form_attachment_response_idx",
+ "columns": [
+ {
+ "expression": "response_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_form_attachment_form_id_knight_hacks_form_schemas_id_fk": {
+ "name": "knight_hacks_form_attachment_form_id_knight_hacks_form_schemas_id_fk",
+ "tableFrom": "knight_hacks_form_attachment",
+ "tableTo": "knight_hacks_form_schemas",
+ "columnsFrom": ["form_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_form_attachment_response_id_knight_hacks_form_response_id_fk": {
+ "name": "knight_hacks_form_attachment_response_id_knight_hacks_form_response_id_fk",
+ "tableFrom": "knight_hacks_form_attachment",
+ "tableTo": "knight_hacks_form_response",
+ "columnsFrom": ["response_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_form_attachment_owner_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_form_attachment_owner_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_form_attachment",
+ "tableTo": "auth_user",
+ "columnsFrom": ["owner_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_form_attachment_objectName_unique": {
+ "name": "knight_hacks_form_attachment_objectName_unique",
+ "nullsNotDistinct": false,
+ "columns": ["object_name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_form_attachment_purpose_check": {
+ "name": "knight_hacks_form_attachment_purpose_check",
+ "value": "\"knight_hacks_form_attachment\".\"purpose\" IN ('instruction', 'response')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_form_callback_configuration": {
+ "name": "knight_hacks_form_callback_configuration",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "form_id": {
+ "name": "form_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "callback_slug": {
+ "name": "callback_slug",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "mappings": {
+ "name": "mappings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_form_callback_configuration_form_id_knight_hacks_form_schemas_id_fk": {
+ "name": "knight_hacks_form_callback_configuration_form_id_knight_hacks_form_schemas_id_fk",
+ "tableFrom": "knight_hacks_form_callback_configuration",
+ "tableTo": "knight_hacks_form_schemas",
+ "columnsFrom": ["form_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_form_callback_configuration_form_slug_unique": {
+ "name": "knight_hacks_form_callback_configuration_form_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": ["form_id", "callback_slug"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_form_callback_execution": {
+ "name": "knight_hacks_form_callback_execution",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "configuration_id": {
+ "name": "configuration_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "response_id": {
+ "name": "response_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "callback_slug": {
+ "name": "callback_slug",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "input": {
+ "name": "input",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "form_callback_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "available_at": {
+ "name": "available_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "lease_token": {
+ "name": "lease_token",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_expires_at": {
+ "name": "lease_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "succeeded_at": {
+ "name": "succeeded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_form_callback_execution_status_available_idx": {
+ "name": "knight_hacks_form_callback_execution_status_available_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "available_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_form_callback_execution_configuration_id_knight_hacks_form_callback_configuration_id_fk": {
+ "name": "knight_hacks_form_callback_execution_configuration_id_knight_hacks_form_callback_configuration_id_fk",
+ "tableFrom": "knight_hacks_form_callback_execution",
+ "tableTo": "knight_hacks_form_callback_configuration",
+ "columnsFrom": ["configuration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_form_callback_execution_response_id_knight_hacks_form_response_id_fk": {
+ "name": "knight_hacks_form_callback_execution_response_id_knight_hacks_form_response_id_fk",
+ "tableFrom": "knight_hacks_form_callback_execution",
+ "tableTo": "knight_hacks_form_response",
+ "columnsFrom": ["response_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_form_response": {
+ "name": "knight_hacks_form_response",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "form": {
+ "name": "form",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "response_data": {
+ "name": "response_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "form_revision": {
+ "name": "form_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "response_snapshot": {
+ "name": "response_snapshot",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "edited_at": {
+ "name": "edited_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_form_response_form_created_idx": {
+ "name": "knight_hacks_form_response_form_created_idx",
+ "columns": [
+ {
+ "expression": "form",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_form_response_user_created_idx": {
+ "name": "knight_hacks_form_response_user_created_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_form_response_form_knight_hacks_form_schemas_id_fk": {
+ "name": "knight_hacks_form_response_form_knight_hacks_form_schemas_id_fk",
+ "tableFrom": "knight_hacks_form_response",
+ "tableTo": "knight_hacks_form_schemas",
+ "columnsFrom": ["form"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_form_response_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_form_response_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_form_response",
+ "tableTo": "auth_user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_form_response_roles": {
+ "name": "knight_hacks_form_response_roles",
+ "schema": "",
+ "columns": {
+ "form_id": {
+ "name": "form_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_form_response_roles_form_id_knight_hacks_form_schemas_id_fk": {
+ "name": "knight_hacks_form_response_roles_form_id_knight_hacks_form_schemas_id_fk",
+ "tableFrom": "knight_hacks_form_response_roles",
+ "tableTo": "knight_hacks_form_schemas",
+ "columnsFrom": ["form_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_form_response_roles_role_id_auth_roles_id_fk": {
+ "name": "knight_hacks_form_response_roles_role_id_auth_roles_id_fk",
+ "tableFrom": "knight_hacks_form_response_roles",
+ "tableTo": "auth_roles",
+ "columnsFrom": ["role_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "knight_hacks_form_response_roles_form_id_role_id_pk": {
+ "name": "knight_hacks_form_response_roles_form_id_role_id_pk",
+ "columns": ["form_id", "role_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_form_section_edit_role": {
+ "name": "knight_hacks_form_section_edit_role",
+ "schema": "",
+ "columns": {
+ "section_id": {
+ "name": "section_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_form_section_edit_role_section_id_knight_hacks_form_sections_id_fk": {
+ "name": "knight_hacks_form_section_edit_role_section_id_knight_hacks_form_sections_id_fk",
+ "tableFrom": "knight_hacks_form_section_edit_role",
+ "tableTo": "knight_hacks_form_sections",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_form_section_edit_role_role_id_auth_roles_id_fk": {
+ "name": "knight_hacks_form_section_edit_role_role_id_auth_roles_id_fk",
+ "tableFrom": "knight_hacks_form_section_edit_role",
+ "tableTo": "auth_roles",
+ "columnsFrom": ["role_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "knight_hacks_form_section_edit_role_section_id_role_id_pk": {
+ "name": "knight_hacks_form_section_edit_role_section_id_role_id_pk",
+ "columns": ["section_id", "role_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_form_section_roles": {
+ "name": "knight_hacks_form_section_roles",
+ "schema": "",
+ "columns": {
+ "section_id": {
+ "name": "section_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_form_section_roles_section_id_knight_hacks_form_sections_id_fk": {
+ "name": "knight_hacks_form_section_roles_section_id_knight_hacks_form_sections_id_fk",
+ "tableFrom": "knight_hacks_form_section_roles",
+ "tableTo": "knight_hacks_form_sections",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_form_section_roles_role_id_auth_roles_id_fk": {
+ "name": "knight_hacks_form_section_roles_role_id_auth_roles_id_fk",
+ "tableFrom": "knight_hacks_form_section_roles",
+ "tableTo": "auth_roles",
+ "columnsFrom": ["role_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "knight_hacks_form_section_roles_section_id_role_id_pk": {
+ "name": "knight_hacks_form_section_roles_section_id_role_id_pk",
+ "columns": ["section_id", "role_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_form_section_view_role": {
+ "name": "knight_hacks_form_section_view_role",
+ "schema": "",
+ "columns": {
+ "section_id": {
+ "name": "section_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_form_section_view_role_section_id_knight_hacks_form_sections_id_fk": {
+ "name": "knight_hacks_form_section_view_role_section_id_knight_hacks_form_sections_id_fk",
+ "tableFrom": "knight_hacks_form_section_view_role",
+ "tableTo": "knight_hacks_form_sections",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_form_section_view_role_role_id_auth_roles_id_fk": {
+ "name": "knight_hacks_form_section_view_role_role_id_auth_roles_id_fk",
+ "tableFrom": "knight_hacks_form_section_view_role",
+ "tableTo": "auth_roles",
+ "columnsFrom": ["role_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "knight_hacks_form_section_view_role_section_id_role_id_pk": {
+ "name": "knight_hacks_form_section_view_role_section_id_role_id_pk",
+ "columns": ["section_id", "role_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_form_sections": {
+ "name": "knight_hacks_form_sections",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_form_sections_name_unique": {
+ "name": "knight_hacks_form_sections_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_form_single_response_claim": {
+ "name": "knight_hacks_form_single_response_claim",
+ "schema": "",
+ "columns": {
+ "form_id": {
+ "name": "form_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "response_id": {
+ "name": "response_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_form_single_response_claim_form_id_knight_hacks_form_schemas_id_fk": {
+ "name": "knight_hacks_form_single_response_claim_form_id_knight_hacks_form_schemas_id_fk",
+ "tableFrom": "knight_hacks_form_single_response_claim",
+ "tableTo": "knight_hacks_form_schemas",
+ "columnsFrom": ["form_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_form_single_response_claim_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_form_single_response_claim_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_form_single_response_claim",
+ "tableTo": "auth_user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_form_single_response_claim_response_id_knight_hacks_form_response_id_fk": {
+ "name": "knight_hacks_form_single_response_claim_response_id_knight_hacks_form_response_id_fk",
+ "tableFrom": "knight_hacks_form_single_response_claim",
+ "tableTo": "knight_hacks_form_response",
+ "columnsFrom": ["response_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "knight_hacks_form_single_response_claim_form_id_user_id_pk": {
+ "name": "knight_hacks_form_single_response_claim_form_id_user_id_pk",
+ "columns": ["form_id", "user_id"]
+ }
+ },
+ "uniqueConstraints": {
+ "knight_hacks_form_single_response_claim_responseId_unique": {
+ "name": "knight_hacks_form_single_response_claim_responseId_unique",
+ "nullsNotDistinct": false,
+ "columns": ["response_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_form_schemas": {
+ "name": "knight_hacks_form_schemas",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug_name": {
+ "name": "slug_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "kind": {
+ "name": "kind",
+ "type": "form_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'general'"
+ },
+ "state": {
+ "name": "state",
+ "type": "form_state",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "opens_at": {
+ "name": "opens_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "closes_at": {
+ "name": "closes_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "manually_closed": {
+ "name": "manually_closed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "response_mode": {
+ "name": "response_mode",
+ "type": "form_response_mode",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'single_locked'"
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "dues_only": {
+ "name": "dues_only",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "allow_resubmission": {
+ "name": "allow_resubmission",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "allow_edit": {
+ "name": "allow_edit",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "form_data": {
+ "name": "form_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "form_validator_json": {
+ "name": "form_validator_json",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "section": {
+ "name": "section",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'General'"
+ },
+ "section_id": {
+ "name": "section_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_closed": {
+ "name": "is_closed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_form_schemas_section_id_knight_hacks_form_sections_id_fk": {
+ "name": "knight_hacks_form_schemas_section_id_knight_hacks_form_sections_id_fk",
+ "tableFrom": "knight_hacks_form_schemas",
+ "tableTo": "knight_hacks_form_sections",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_form_schemas_slugName_unique": {
+ "name": "knight_hacks_form_schemas_slugName_unique",
+ "nullsNotDistinct": false,
+ "columns": ["slug_name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hackathon": {
+ "name": "knight_hacks_hackathon",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "theme": {
+ "name": "theme",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'America/New_York'"
+ },
+ "application_url": {
+ "name": "application_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "application_background_enabled": {
+ "name": "application_background_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "application_background_key": {
+ "name": "application_background_key",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_template_enabled": {
+ "name": "email_template_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "email_template_key": {
+ "name": "email_template_key",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "application_open": {
+ "name": "application_open",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "application_deadline": {
+ "name": "application_deadline",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "confirmation_deadline": {
+ "name": "confirmation_deadline",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "start_date": {
+ "name": "start_date",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_date": {
+ "name": "end_date",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "confirmation_capacity": {
+ "name": "confirmation_capacity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "general_hacker_discord_role_id": {
+ "name": "general_hacker_discord_role_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "event_announcement_channel_id": {
+ "name": "event_announcement_channel_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hackathon_name_unique": {
+ "name": "knight_hacks_hackathon_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hackathon_event_announcement_channel_id_check": {
+ "name": "knight_hacks_hackathon_event_announcement_channel_id_check",
+ "value": "\"knight_hacks_hackathon\".\"event_announcement_channel_id\" IS NULL OR \"knight_hacks_hackathon\".\"event_announcement_channel_id\" ~ '^[0-9]{17,20}$'"
+ },
+ "knight_hacks_hackathon_general_hacker_discord_role_id_check": {
+ "name": "knight_hacks_hackathon_general_hacker_discord_role_id_check",
+ "value": "\"knight_hacks_hackathon\".\"general_hacker_discord_role_id\" IS NULL OR \"knight_hacks_hackathon\".\"general_hacker_discord_role_id\" ~ '^[0-9]{17,20}$'"
+ },
+ "knight_hacks_hackathon_confirmation_capacity_check": {
+ "name": "knight_hacks_hackathon_confirmation_capacity_check",
+ "value": "\"knight_hacks_hackathon\".\"confirmation_capacity\" IS NULL OR \"knight_hacks_hackathon\".\"confirmation_capacity\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hackathon_agreement_definition": {
+ "name": "knight_hacks_hackathon_agreement_definition",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stage": {
+ "name": "stage",
+ "type": "hacker_agreement_stage",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "legal_text": {
+ "name": "legal_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "required": {
+ "name": "required",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "knight_hacks_hackathon_agreement_definition_active_unique": {
+ "name": "knight_hacks_hackathon_agreement_definition_active_unique",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "stage",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knight_hacks_hackathon_agreement_definition\".\"active\" = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hackathon_agreement_definition_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_hackathon_agreement_definition_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_hackathon_agreement_definition",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hackathon_agreement_definition_created_by_auth_user_id_fk": {
+ "name": "knight_hacks_hackathon_agreement_definition_created_by_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hackathon_agreement_definition",
+ "tableTo": "auth_user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hackathon_agreement_definition_version_unique": {
+ "name": "knight_hacks_hackathon_agreement_definition_version_unique",
+ "nullsNotDistinct": false,
+ "columns": ["hackathon_id", "stage", "key", "version"]
+ },
+ "knight_hacks_hackathon_agreement_definition_id_hackathon_unique": {
+ "name": "knight_hacks_hackathon_agreement_definition_id_hackathon_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "hackathon_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hackathon_agreement_definition_content_check": {
+ "name": "knight_hacks_hackathon_agreement_definition_content_check",
+ "value": "\"knight_hacks_hackathon_agreement_definition\".\"legal_text\" IS NOT NULL OR \"knight_hacks_hackathon_agreement_definition\".\"url\" IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hackathon_class": {
+ "name": "knight_hacks_hackathon_class",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "discord_role_id": {
+ "name": "discord_role_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(7)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "knight_hacks_hackathon_class_hackathon_idx": {
+ "name": "knight_hacks_hackathon_class_hackathon_idx",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hackathon_class_one_vip_per_hackathon": {
+ "name": "knight_hacks_hackathon_class_one_vip_per_hackathon",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knight_hacks_hackathon_class\".\"kind\" = 'vip'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hackathon_class_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_hackathon_class_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_hackathon_class",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hackathon_class_id_hackathon_unique": {
+ "name": "knight_hacks_hackathon_class_id_hackathon_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "hackathon_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hackathon_class_discord_role_id_check": {
+ "name": "knight_hacks_hackathon_class_discord_role_id_check",
+ "value": "\"knight_hacks_hackathon_class\".\"discord_role_id\" ~ '^[0-9]{17,20}$'"
+ },
+ "knight_hacks_hackathon_class_color_check": {
+ "name": "knight_hacks_hackathon_class_color_check",
+ "value": "\"knight_hacks_hackathon_class\".\"color\" ~ '^#[0-9a-fA-F]{6}$'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hackathon_event_publication": {
+ "name": "knight_hacks_hackathon_event_publication",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "hackathon_event_publication_provider",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "desired_enabled": {
+ "name": "desired_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "requested_at": {
+ "name": "requested_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "requested_by": {
+ "name": "requested_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_reconciled_at": {
+ "name": "last_reconciled_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_converged_at": {
+ "name": "last_converged_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_hackathon_event_publication_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_hackathon_event_publication_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_hackathon_event_publication",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hackathon_event_publication_requested_by_auth_user_id_fk": {
+ "name": "knight_hacks_hackathon_event_publication_requested_by_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hackathon_event_publication",
+ "tableTo": "auth_user",
+ "columnsFrom": ["requested_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hackathon_event_publication_provider_unique": {
+ "name": "knight_hacks_hackathon_event_publication_provider_unique",
+ "nullsNotDistinct": false,
+ "columns": ["hackathon_id", "provider"]
+ },
+ "knight_hacks_hackathon_event_publication_id_scope_unique": {
+ "name": "knight_hacks_hackathon_event_publication_id_scope_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "hackathon_id", "provider"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hackathon_event_publication_revision_check": {
+ "name": "knight_hacks_hackathon_event_publication_revision_check",
+ "value": "\"knight_hacks_hackathon_event_publication\".\"revision\" >= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hackathon_event_reminder_delivery": {
+ "name": "knight_hacks_hackathon_event_reminder_delivery",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_start_at": {
+ "name": "event_start_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reminder_key": {
+ "name": "reminder_key",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destination_channel_id_snapshot": {
+ "name": "destination_channel_id_snapshot",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role_id_snapshot": {
+ "name": "role_id_snapshot",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "discord_event_id_snapshot": {
+ "name": "discord_event_id_snapshot",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content_snapshot": {
+ "name": "content_snapshot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "hackathon_event_reminder_state",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locked_at": {
+ "name": "locked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivered_at": {
+ "name": "delivered_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_hackathon_event_reminder_delivery_pending_idx": {
+ "name": "knight_hacks_hackathon_event_reminder_delivery_pending_idx",
+ "columns": [
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hackathon_event_reminder_scoped_event_fk": {
+ "name": "knight_hacks_hackathon_event_reminder_scoped_event_fk",
+ "tableFrom": "knight_hacks_hackathon_event_reminder_delivery",
+ "tableTo": "knight_hacks_event",
+ "columnsFrom": ["event_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hackathon_event_reminder_delivery_identity_unique": {
+ "name": "knight_hacks_hackathon_event_reminder_delivery_identity_unique",
+ "nullsNotDistinct": false,
+ "columns": ["event_id", "reminder_key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hackathon_event_reminder_channel_id_check": {
+ "name": "knight_hacks_hackathon_event_reminder_channel_id_check",
+ "value": "\"knight_hacks_hackathon_event_reminder_delivery\".\"destination_channel_id_snapshot\" ~ '^[0-9]{17,20}$'"
+ },
+ "knight_hacks_hackathon_event_reminder_role_id_check": {
+ "name": "knight_hacks_hackathon_event_reminder_role_id_check",
+ "value": "\"knight_hacks_hackathon_event_reminder_delivery\".\"role_id_snapshot\" ~ '^[0-9]{17,20}$'"
+ },
+ "knight_hacks_hackathon_event_reminder_attempt_count_check": {
+ "name": "knight_hacks_hackathon_event_reminder_attempt_count_check",
+ "value": "\"knight_hacks_hackathon_event_reminder_delivery\".\"attempt_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hackathon_portal_authorization_code": {
+ "name": "knight_hacks_hackathon_portal_authorization_code",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "portal_client_id": {
+ "name": "portal_client_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "better_auth_session_id": {
+ "name": "better_auth_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code_hash": {
+ "name": "code_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code_challenge": {
+ "name": "code_challenge",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code_challenge_method": {
+ "name": "code_challenge_method",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'S256'"
+ },
+ "redirect_uri": {
+ "name": "redirect_uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "consumed_at": {
+ "name": "consumed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_hackathon_portal_authorization_code_expiry_idx": {
+ "name": "knight_hacks_hackathon_portal_authorization_code_expiry_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hackathon_portal_authorization_code_client_expiry_idx": {
+ "name": "knight_hacks_hackathon_portal_authorization_code_client_expiry_idx",
+ "columns": [
+ {
+ "expression": "portal_client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hackathon_portal_authorization_code_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_hackathon_portal_authorization_code_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hackathon_portal_authorization_code",
+ "tableTo": "auth_user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hackathon_portal_authorization_code_better_auth_session_id_auth_session_id_fk": {
+ "name": "knight_hacks_hackathon_portal_authorization_code_better_auth_session_id_auth_session_id_fk",
+ "tableFrom": "knight_hacks_hackathon_portal_authorization_code",
+ "tableTo": "auth_session",
+ "columnsFrom": ["better_auth_session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hackathon_portal_authorization_code_scoped_client_fk": {
+ "name": "knight_hacks_hackathon_portal_authorization_code_scoped_client_fk",
+ "tableFrom": "knight_hacks_hackathon_portal_authorization_code",
+ "tableTo": "knight_hacks_hackathon_portal_client",
+ "columnsFrom": ["portal_client_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hackathon_portal_authorization_code_hash_unique": {
+ "name": "knight_hacks_hackathon_portal_authorization_code_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["code_hash"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hackathon_portal_authorization_code_hash_check": {
+ "name": "knight_hacks_hackathon_portal_authorization_code_hash_check",
+ "value": "\"knight_hacks_hackathon_portal_authorization_code\".\"code_hash\" ~ '^[0-9a-f]{64}$'"
+ },
+ "knight_hacks_hackathon_portal_authorization_code_method_check": {
+ "name": "knight_hacks_hackathon_portal_authorization_code_method_check",
+ "value": "\"knight_hacks_hackathon_portal_authorization_code\".\"code_challenge_method\" = 'S256'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hackathon_portal_client": {
+ "name": "knight_hacks_hackathon_portal_client",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "production_origin": {
+ "name": "production_origin",
+ "type": "varchar(2048)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_hackathon_portal_client_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_hackathon_portal_client_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_hackathon_portal_client",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hackathon_portal_client_created_by_auth_user_id_fk": {
+ "name": "knight_hacks_hackathon_portal_client_created_by_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hackathon_portal_client",
+ "tableTo": "auth_user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hackathon_portal_client_client_id_unique": {
+ "name": "knight_hacks_hackathon_portal_client_client_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["client_id"]
+ },
+ "knight_hacks_hackathon_portal_client_hackathon_unique": {
+ "name": "knight_hacks_hackathon_portal_client_hackathon_unique",
+ "nullsNotDistinct": false,
+ "columns": ["hackathon_id"]
+ },
+ "knight_hacks_hackathon_portal_client_origin_unique": {
+ "name": "knight_hacks_hackathon_portal_client_origin_unique",
+ "nullsNotDistinct": false,
+ "columns": ["production_origin"]
+ },
+ "knight_hacks_hackathon_portal_client_id_hackathon_unique": {
+ "name": "knight_hacks_hackathon_portal_client_id_hackathon_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "hackathon_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hackathon_portal_client_origin_check": {
+ "name": "knight_hacks_hackathon_portal_client_origin_check",
+ "value": "\"knight_hacks_hackathon_portal_client\".\"production_origin\" ~ '^https://[a-z0-9-]+([.][a-z0-9-]+)*[.]knighthacks[.]org$'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hackathon_portal_session": {
+ "name": "knight_hacks_hackathon_portal_session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "portal_client_id": {
+ "name": "portal_client_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "better_auth_session_id": {
+ "name": "better_auth_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token_hash": {
+ "name": "access_token_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_token_hash": {
+ "name": "refresh_token_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_expires_at": {
+ "name": "access_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_expires_at": {
+ "name": "refresh_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_version": {
+ "name": "refresh_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_hackathon_portal_session_access_expiry_idx": {
+ "name": "knight_hacks_hackathon_portal_session_access_expiry_idx",
+ "columns": [
+ {
+ "expression": "access_expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "revoked_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hackathon_portal_session_refresh_expiry_idx": {
+ "name": "knight_hacks_hackathon_portal_session_refresh_expiry_idx",
+ "columns": [
+ {
+ "expression": "refresh_expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hackathon_portal_session_user_client_idx": {
+ "name": "knight_hacks_hackathon_portal_session_user_client_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "portal_client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hackathon_portal_session_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_hackathon_portal_session_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hackathon_portal_session",
+ "tableTo": "auth_user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hackathon_portal_session_better_auth_session_id_auth_session_id_fk": {
+ "name": "knight_hacks_hackathon_portal_session_better_auth_session_id_auth_session_id_fk",
+ "tableFrom": "knight_hacks_hackathon_portal_session",
+ "tableTo": "auth_session",
+ "columnsFrom": ["better_auth_session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hackathon_portal_session_scoped_client_fk": {
+ "name": "knight_hacks_hackathon_portal_session_scoped_client_fk",
+ "tableFrom": "knight_hacks_hackathon_portal_session",
+ "tableTo": "knight_hacks_hackathon_portal_client",
+ "columnsFrom": ["portal_client_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hackathon_portal_session_access_hash_unique": {
+ "name": "knight_hacks_hackathon_portal_session_access_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["access_token_hash"]
+ },
+ "knight_hacks_hackathon_portal_session_refresh_hash_unique": {
+ "name": "knight_hacks_hackathon_portal_session_refresh_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["refresh_token_hash"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hackathon_portal_session_access_hash_check": {
+ "name": "knight_hacks_hackathon_portal_session_access_hash_check",
+ "value": "\"knight_hacks_hackathon_portal_session\".\"access_token_hash\" ~ '^[0-9a-f]{64}$'"
+ },
+ "knight_hacks_hackathon_portal_session_refresh_hash_check": {
+ "name": "knight_hacks_hackathon_portal_session_refresh_hash_check",
+ "value": "\"knight_hacks_hackathon_portal_session\".\"refresh_token_hash\" ~ '^[0-9a-f]{64}$'"
+ },
+ "knight_hacks_hackathon_portal_session_refresh_version_check": {
+ "name": "knight_hacks_hackathon_portal_session_refresh_version_check",
+ "value": "\"knight_hacks_hackathon_portal_session\".\"refresh_version\" >= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hackathon_portal_session_credential": {
+ "name": "knight_hacks_hackathon_portal_session_credential",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "portal_session_id": {
+ "name": "portal_session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_kind": {
+ "name": "token_kind",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rotated_at": {
+ "name": "rotated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_hackathon_portal_session_credential_session_idx": {
+ "name": "knight_hacks_hackathon_portal_session_credential_session_idx",
+ "columns": [
+ {
+ "expression": "portal_session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hackathon_portal_session_credential_portal_session_id_knight_hacks_hackathon_portal_session_id_fk": {
+ "name": "knight_hacks_hackathon_portal_session_credential_portal_session_id_knight_hacks_hackathon_portal_session_id_fk",
+ "tableFrom": "knight_hacks_hackathon_portal_session_credential",
+ "tableTo": "knight_hacks_hackathon_portal_session",
+ "columnsFrom": ["portal_session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hackathon_portal_session_credential_hash_unique": {
+ "name": "knight_hacks_hackathon_portal_session_credential_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token_hash"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hackathon_portal_session_credential_hash_check": {
+ "name": "knight_hacks_hackathon_portal_session_credential_hash_check",
+ "value": "\"knight_hacks_hackathon_portal_session_credential\".\"token_hash\" ~ '^[0-9a-f]{64}$'"
+ },
+ "knight_hacks_hackathon_portal_session_credential_kind_check": {
+ "name": "knight_hacks_hackathon_portal_session_credential_kind_check",
+ "value": "\"knight_hacks_hackathon_portal_session_credential\".\"token_kind\" IN ('access', 'refresh')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hackathon_sponsor": {
+ "name": "knight_hacks_hackathon_sponsor",
+ "schema": "",
+ "columns": {
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sponsor_id": {
+ "name": "sponsor_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tier": {
+ "name": "tier",
+ "type": "sponsor_tier",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_hackathon_sponsor_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_hackathon_sponsor_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_hackathon_sponsor",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hackathon_sponsor_sponsor_id_knight_hacks_sponsor_id_fk": {
+ "name": "knight_hacks_hackathon_sponsor_sponsor_id_knight_hacks_sponsor_id_fk",
+ "tableFrom": "knight_hacks_hackathon_sponsor",
+ "tableTo": "knight_hacks_sponsor",
+ "columnsFrom": ["sponsor_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hackathon_status_email": {
+ "name": "knight_hacks_hackathon_status_email",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_hackathon_status_email_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_hackathon_status_email_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_hackathon_status_email",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hackathon_status_email_template_id_email_template_id_fk": {
+ "name": "knight_hacks_hackathon_status_email_template_id_email_template_id_fk",
+ "tableFrom": "knight_hacks_hackathon_status_email",
+ "tableTo": "email_template",
+ "columnsFrom": ["template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hackathon_status_email_hackathon_status_unique": {
+ "name": "knight_hacks_hackathon_status_email_hackathon_status_unique",
+ "nullsNotDistinct": false,
+ "columns": ["hackathon_id", "status"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hackathon_status_email_status_check": {
+ "name": "knight_hacks_hackathon_status_email_status_check",
+ "value": "\"knight_hacks_hackathon_status_email\".\"status\" <> 'checkedin'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hacker": {
+ "name": "knight_hacks_hacker",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "gender": {
+ "name": "gender",
+ "type": "gender",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Prefer not to answer'"
+ },
+ "discord_user": {
+ "name": "discord_user",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "age": {
+ "name": "age",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "country": {
+ "name": "country",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'United States of America'"
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "phone_number": {
+ "name": "phone_number",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "school": {
+ "name": "school",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "level_of_study": {
+ "name": "level_of_study",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "major": {
+ "name": "major",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Computer Science'"
+ },
+ "race_or_ethnicity": {
+ "name": "race_or_ethnicity",
+ "type": "race_or_ethnicity",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Prefer not to answer'"
+ },
+ "shirt_size": {
+ "name": "shirt_size",
+ "type": "shirt_size",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "github_profile_url": {
+ "name": "github_profile_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "linkedin_profile_url": {
+ "name": "linkedin_profile_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "website_url": {
+ "name": "website_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resume_url": {
+ "name": "resume_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dob": {
+ "name": "dob",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "grad_date": {
+ "name": "grad_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "survey_1": {
+ "name": "survey_1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "survey_2": {
+ "name": "survey_2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_first_time": {
+ "name": "is_first_time",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "food_allergies": {
+ "name": "food_allergies",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agrees_to_receive_emails_from_mlh": {
+ "name": "agrees_to_receive_emails_from_mlh",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "agrees_to_mlh_code_of_conduct": {
+ "name": "agrees_to_mlh_code_of_conduct",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "agrees_to_mlh_data_sharing": {
+ "name": "agrees_to_mlh_data_sharing",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "date_created": {
+ "name": "date_created",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "time",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_hacker_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_hacker_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hacker",
+ "tableTo": "auth_user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hacker_agreement_acceptance": {
+ "name": "knight_hacks_hacker_agreement_acceptance",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "attendee_id": {
+ "name": "attendee_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agreement_definition_id": {
+ "name": "agreement_definition_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "accepted": {
+ "name": "accepted",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provenance": {
+ "name": "provenance",
+ "type": "hacker_agreement_provenance",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'explicit'"
+ },
+ "accepted_at": {
+ "name": "accepted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "recorded_at": {
+ "name": "recorded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_hacker_agreement_acceptance_attendee_idx": {
+ "name": "knight_hacks_hacker_agreement_acceptance_attendee_idx",
+ "columns": [
+ {
+ "expression": "attendee_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hacker_agreement_acceptance_scoped_attendee_fk": {
+ "name": "knight_hacks_hacker_agreement_acceptance_scoped_attendee_fk",
+ "tableFrom": "knight_hacks_hacker_agreement_acceptance",
+ "tableTo": "knight_hacks_hacker_attendee",
+ "columnsFrom": ["attendee_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_agreement_acceptance_scoped_definition_fk": {
+ "name": "knight_hacks_hacker_agreement_acceptance_scoped_definition_fk",
+ "tableFrom": "knight_hacks_hacker_agreement_acceptance",
+ "tableTo": "knight_hacks_hackathon_agreement_definition",
+ "columnsFrom": ["agreement_definition_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hacker_agreement_acceptance_attendee_definition_unique": {
+ "name": "knight_hacks_hacker_agreement_acceptance_attendee_definition_unique",
+ "nullsNotDistinct": false,
+ "columns": ["attendee_id", "agreement_definition_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hacker_agreement_acceptance_timestamp_check": {
+ "name": "knight_hacks_hacker_agreement_acceptance_timestamp_check",
+ "value": "(\"knight_hacks_hacker_agreement_acceptance\".\"accepted\" = true AND \"knight_hacks_hacker_agreement_acceptance\".\"accepted_at\" IS NOT NULL) OR (\"knight_hacks_hacker_agreement_acceptance\".\"accepted\" = false AND \"knight_hacks_hacker_agreement_acceptance\".\"accepted_at\" IS NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hacker_attendee": {
+ "name": "knight_hacks_hacker_attendee",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "hacker_id": {
+ "name": "hacker_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile_id": {
+ "name": "profile_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "profile_revision_id": {
+ "name": "profile_revision_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "time_applied": {
+ "name": "time_applied",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "time_confirmed": {
+ "name": "time_confirmed",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "points": {
+ "name": "points",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "class_id": {
+ "name": "class_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_vip": {
+ "name": "is_vip",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_first_time": {
+ "name": "is_first_time",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "survey1": {
+ "name": "survey1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "survey2": {
+ "name": "survey2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "checked_in_at": {
+ "name": "checked_in_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "checked_in_by": {
+ "name": "checked_in_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "class": {
+ "name": "class",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": null
+ },
+ "blacklisted_at": {
+ "name": "blacklisted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "blacklisted_by": {
+ "name": "blacklisted_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "blacklist_reason": {
+ "name": "blacklist_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_status_send_id": {
+ "name": "last_status_send_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "knight_hacks_hacker_attendee_class_idx": {
+ "name": "knight_hacks_hacker_attendee_class_idx",
+ "columns": [
+ {
+ "expression": "class_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_attendee_hackathon_class_idx": {
+ "name": "knight_hacks_hacker_attendee_hackathon_class_idx",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "class_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_attendee_checked_in_by_idx": {
+ "name": "knight_hacks_hacker_attendee_checked_in_by_idx",
+ "columns": [
+ {
+ "expression": "checked_in_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_attendee_hackathon_checked_in_idx": {
+ "name": "knight_hacks_hacker_attendee_hackathon_checked_in_idx",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "checked_in_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_attendee_profile_hackathon_unique": {
+ "name": "knight_hacks_hacker_attendee_profile_hackathon_unique",
+ "columns": [
+ {
+ "expression": "profile_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knight_hacks_hacker_attendee\".\"profile_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_attendee_profile_idx": {
+ "name": "knight_hacks_hacker_attendee_profile_idx",
+ "columns": [
+ {
+ "expression": "profile_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_attendee_profile_revision_idx": {
+ "name": "knight_hacks_hacker_attendee_profile_revision_idx",
+ "columns": [
+ {
+ "expression": "profile_revision_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_attendee_hackathon_status_idx": {
+ "name": "knight_hacks_hacker_attendee_hackathon_status_idx",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_attendee_blacklisted_by_idx": {
+ "name": "knight_hacks_hacker_attendee_blacklisted_by_idx",
+ "columns": [
+ {
+ "expression": "blacklisted_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_attendee_last_status_send_idx": {
+ "name": "knight_hacks_hacker_attendee_last_status_send_idx",
+ "columns": [
+ {
+ "expression": "last_status_send_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hacker_attendee_hacker_id_knight_hacks_hacker_id_fk": {
+ "name": "knight_hacks_hacker_attendee_hacker_id_knight_hacks_hacker_id_fk",
+ "tableFrom": "knight_hacks_hacker_attendee",
+ "tableTo": "knight_hacks_hacker",
+ "columnsFrom": ["hacker_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_attendee_profile_id_knight_hacks_hacker_profile_id_fk": {
+ "name": "knight_hacks_hacker_attendee_profile_id_knight_hacks_hacker_profile_id_fk",
+ "tableFrom": "knight_hacks_hacker_attendee",
+ "tableTo": "knight_hacks_hacker_profile",
+ "columnsFrom": ["profile_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_attendee_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_hacker_attendee_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_hacker_attendee",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_attendee_checked_in_by_auth_user_id_fk": {
+ "name": "knight_hacks_hacker_attendee_checked_in_by_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hacker_attendee",
+ "tableTo": "auth_user",
+ "columnsFrom": ["checked_in_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_attendee_blacklisted_by_auth_user_id_fk": {
+ "name": "knight_hacks_hacker_attendee_blacklisted_by_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hacker_attendee",
+ "tableTo": "auth_user",
+ "columnsFrom": ["blacklisted_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_attendee_last_status_send_id_email_send_id_fk": {
+ "name": "knight_hacks_hacker_attendee_last_status_send_id_email_send_id_fk",
+ "tableFrom": "knight_hacks_hacker_attendee",
+ "tableTo": "email_send",
+ "columnsFrom": ["last_status_send_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_attendee_scoped_class_fk": {
+ "name": "knight_hacks_hacker_attendee_scoped_class_fk",
+ "tableFrom": "knight_hacks_hacker_attendee",
+ "tableTo": "knight_hacks_hackathon_class",
+ "columnsFrom": ["class_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_attendee_scoped_profile_revision_fk": {
+ "name": "knight_hacks_hacker_attendee_scoped_profile_revision_fk",
+ "tableFrom": "knight_hacks_hacker_attendee",
+ "tableTo": "knight_hacks_hacker_profile_revision",
+ "columnsFrom": ["profile_revision_id", "profile_id"],
+ "columnsTo": ["id", "profile_id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hacker_attendee_hacker_hackathon_unique": {
+ "name": "knight_hacks_hacker_attendee_hacker_hackathon_unique",
+ "nullsNotDistinct": false,
+ "columns": ["hacker_id", "hackathon_id"]
+ },
+ "knight_hacks_hacker_attendee_id_hackathon_unique": {
+ "name": "knight_hacks_hacker_attendee_id_hackathon_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "hackathon_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hacker_attendee_blacklist_reason_check": {
+ "name": "knight_hacks_hacker_attendee_blacklist_reason_check",
+ "value": "(\"knight_hacks_hacker_attendee\".\"blacklisted_at\" is null and \"knight_hacks_hacker_attendee\".\"blacklist_reason\" is null)\n or (\"knight_hacks_hacker_attendee\".\"blacklisted_at\" is not null and \"knight_hacks_hacker_attendee\".\"blacklist_reason\" is not null)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hacker_check_in_attempt": {
+ "name": "knight_hacks_hacker_check_in_attempt",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hacker_attendee_id": {
+ "name": "hacker_attendee_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attendance_id": {
+ "name": "attendance_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "operator_id": {
+ "name": "operator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "operator_display_name_snapshot": {
+ "name": "operator_display_name_snapshot",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mode": {
+ "name": "mode",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "outcome": {
+ "name": "outcome",
+ "type": "hacker_check_in_outcome",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_purpose": {
+ "name": "event_purpose",
+ "type": "event_purpose",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_name_snapshot": {
+ "name": "event_name_snapshot",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hacker_name_snapshot": {
+ "name": "hacker_name_snapshot",
+ "type": "varchar(511)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "class_id": {
+ "name": "class_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "class_name_snapshot": {
+ "name": "class_name_snapshot",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "class_color_snapshot": {
+ "name": "class_color_snapshot",
+ "type": "varchar(7)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_vip_snapshot": {
+ "name": "is_vip_snapshot",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "was_minor_at_attempt": {
+ "name": "was_minor_at_attempt",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_repeat_occurrence": {
+ "name": "is_repeat_occurrence",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "points_awarded": {
+ "name": "points_awarded",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "attempted_at": {
+ "name": "attempted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "knight_hacks_hacker_check_in_attempt_hackathon_history_idx": {
+ "name": "knight_hacks_hacker_check_in_attempt_hackathon_history_idx",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "attempted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_check_in_attempt_attendee_history_idx": {
+ "name": "knight_hacks_hacker_check_in_attempt_attendee_history_idx",
+ "columns": [
+ {
+ "expression": "hacker_attendee_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "attempted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_check_in_attempt_expiring_idx": {
+ "name": "knight_hacks_hacker_check_in_attempt_expiring_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knight_hacks_hacker_check_in_attempt\".\"expires_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hacker_check_in_attempt_operator_id_auth_user_id_fk": {
+ "name": "knight_hacks_hacker_check_in_attempt_operator_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hacker_check_in_attempt",
+ "tableTo": "auth_user",
+ "columnsFrom": ["operator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_check_in_attempt_scoped_attendee_fk": {
+ "name": "knight_hacks_hacker_check_in_attempt_scoped_attendee_fk",
+ "tableFrom": "knight_hacks_hacker_check_in_attempt",
+ "tableTo": "knight_hacks_hacker_attendee",
+ "columnsFrom": ["hacker_attendee_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_check_in_attempt_scoped_attendance_fk": {
+ "name": "knight_hacks_hacker_check_in_attempt_scoped_attendance_fk",
+ "tableFrom": "knight_hacks_hacker_check_in_attempt",
+ "tableTo": "knight_hacks_hacker_event_attendee",
+ "columnsFrom": [
+ "attendance_id",
+ "hackathon_id",
+ "event_id",
+ "hacker_attendee_id"
+ ],
+ "columnsTo": ["id", "hackathon_id", "event_id", "hacker_att_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_check_in_attempt_scoped_class_fk": {
+ "name": "knight_hacks_hacker_check_in_attempt_scoped_class_fk",
+ "tableFrom": "knight_hacks_hacker_check_in_attempt",
+ "tableTo": "knight_hacks_hackathon_class",
+ "columnsFrom": ["class_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_check_in_attempt_scoped_event_fk": {
+ "name": "knight_hacks_hacker_check_in_attempt_scoped_event_fk",
+ "tableFrom": "knight_hacks_hacker_check_in_attempt",
+ "tableTo": "knight_hacks_event",
+ "columnsFrom": ["event_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hacker_check_in_attempt_attendanceId_unique": {
+ "name": "knight_hacks_hacker_check_in_attempt_attendanceId_unique",
+ "nullsNotDistinct": false,
+ "columns": ["attendance_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hacker_check_in_attempt_mode_check": {
+ "name": "knight_hacks_hacker_check_in_attempt_mode_check",
+ "value": "\"knight_hacks_hacker_check_in_attempt\".\"mode\" IN ('scanner', 'manual')"
+ },
+ "knight_hacks_hacker_check_in_attempt_class_color_check": {
+ "name": "knight_hacks_hacker_check_in_attempt_class_color_check",
+ "value": "\"knight_hacks_hacker_check_in_attempt\".\"class_color_snapshot\" IS NULL OR \"knight_hacks_hacker_check_in_attempt\".\"class_color_snapshot\" ~ '^#[0-9A-Fa-f]{6}$'"
+ },
+ "knight_hacks_hacker_check_in_attempt_points_check": {
+ "name": "knight_hacks_hacker_check_in_attempt_points_check",
+ "value": "\"knight_hacks_hacker_check_in_attempt\".\"points_awarded\" >= 0"
+ },
+ "knight_hacks_hacker_check_in_attempt_success_retention_check": {
+ "name": "knight_hacks_hacker_check_in_attempt_success_retention_check",
+ "value": "(\"knight_hacks_hacker_check_in_attempt\".\"outcome\" = 'checked_in' AND \"knight_hacks_hacker_check_in_attempt\".\"attendance_id\" IS NOT NULL AND \"knight_hacks_hacker_check_in_attempt\".\"expires_at\" IS NULL) OR (\"knight_hacks_hacker_check_in_attempt\".\"outcome\" <> 'checked_in' AND \"knight_hacks_hacker_check_in_attempt\".\"attendance_id\" IS NULL AND \"knight_hacks_hacker_check_in_attempt\".\"expires_at\" IS NOT NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hacker_check_in_pass": {
+ "name": "knight_hacks_hacker_check_in_pass",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "attendee_id": {
+ "name": "attendee_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "issued_at": {
+ "name": "issued_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "knight_hacks_hacker_check_in_pass_active_attendee_unique": {
+ "name": "knight_hacks_hacker_check_in_pass_active_attendee_unique",
+ "columns": [
+ {
+ "expression": "attendee_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knight_hacks_hacker_check_in_pass\".\"revoked_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_check_in_pass_attendee_idx": {
+ "name": "knight_hacks_hacker_check_in_pass_attendee_idx",
+ "columns": [
+ {
+ "expression": "attendee_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hacker_check_in_pass_scoped_attendee_fk": {
+ "name": "knight_hacks_hacker_check_in_pass_scoped_attendee_fk",
+ "tableFrom": "knight_hacks_hacker_check_in_pass",
+ "tableTo": "knight_hacks_hacker_attendee",
+ "columnsFrom": ["attendee_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hacker_check_in_pass_token_hash_unique": {
+ "name": "knight_hacks_hacker_check_in_pass_token_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token_hash"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hacker_check_in_pass_token_hash_check": {
+ "name": "knight_hacks_hacker_check_in_pass_token_hash_check",
+ "value": "\"knight_hacks_hacker_check_in_pass\".\"token_hash\" ~ '^[0-9a-f]{64}$'"
+ },
+ "knight_hacks_hacker_check_in_pass_version_check": {
+ "name": "knight_hacks_hacker_check_in_pass_version_check",
+ "value": "\"knight_hacks_hacker_check_in_pass\".\"version\" >= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hacker_discord_role_grant": {
+ "name": "knight_hacks_hacker_discord_role_grant",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "hacker_attendee_id": {
+ "name": "hacker_attendee_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_attendance_id": {
+ "name": "source_attendance_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_event_id": {
+ "name": "source_event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "hacker_discord_role_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "desired_role_id": {
+ "name": "desired_role_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "hacker_discord_role_grant_state",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_attempt_at": {
+ "name": "last_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "succeeded_at": {
+ "name": "succeeded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_token": {
+ "name": "lease_token",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_expires_at": {
+ "name": "lease_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "knight_hacks_hacker_discord_role_grant_repair_queue_idx": {
+ "name": "knight_hacks_hacker_discord_role_grant_repair_queue_idx",
+ "columns": [
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lease_expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hacker_discord_role_grant_scoped_attendee_fk": {
+ "name": "knight_hacks_hacker_discord_role_grant_scoped_attendee_fk",
+ "tableFrom": "knight_hacks_hacker_discord_role_grant",
+ "tableTo": "knight_hacks_hacker_attendee",
+ "columnsFrom": ["hacker_attendee_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_discord_role_grant_scoped_attendance_fk": {
+ "name": "knight_hacks_hacker_discord_role_grant_scoped_attendance_fk",
+ "tableFrom": "knight_hacks_hacker_discord_role_grant",
+ "tableTo": "knight_hacks_hacker_event_attendee",
+ "columnsFrom": [
+ "source_attendance_id",
+ "hackathon_id",
+ "source_event_id",
+ "hacker_attendee_id"
+ ],
+ "columnsTo": ["id", "hackathon_id", "event_id", "hacker_att_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hacker_discord_role_grant_logical_unique": {
+ "name": "knight_hacks_hacker_discord_role_grant_logical_unique",
+ "nullsNotDistinct": false,
+ "columns": ["hacker_attendee_id", "kind"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hacker_discord_role_grant_role_id_check": {
+ "name": "knight_hacks_hacker_discord_role_grant_role_id_check",
+ "value": "\"knight_hacks_hacker_discord_role_grant\".\"desired_role_id\" ~ '^[0-9]{17,20}$'"
+ },
+ "knight_hacks_hacker_discord_role_grant_attempt_count_check": {
+ "name": "knight_hacks_hacker_discord_role_grant_attempt_count_check",
+ "value": "\"knight_hacks_hacker_discord_role_grant\".\"attempt_count\" >= 0"
+ },
+ "knight_hacks_hacker_discord_role_grant_lease_pair_check": {
+ "name": "knight_hacks_hacker_discord_role_grant_lease_pair_check",
+ "value": "(\"knight_hacks_hacker_discord_role_grant\".\"lease_token\" IS NULL) = (\"knight_hacks_hacker_discord_role_grant\".\"lease_expires_at\" IS NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hacker_discord_role_grant_attempt": {
+ "name": "knight_hacks_hacker_discord_role_grant_attempt",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "grant_id": {
+ "name": "grant_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attempt_token": {
+ "name": "attempt_token",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role_id_snapshot": {
+ "name": "role_id_snapshot",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "discord_user_id_snapshot": {
+ "name": "discord_user_id_snapshot",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attempted_by": {
+ "name": "attempted_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "outcome": {
+ "name": "outcome",
+ "type": "hacker_discord_role_attempt_outcome",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "error": {
+ "name": "error",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "knight_hacks_hacker_discord_role_grant_attempt_history_idx": {
+ "name": "knight_hacks_hacker_discord_role_grant_attempt_history_idx",
+ "columns": [
+ {
+ "expression": "grant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hacker_discord_role_grant_attempt_grant_id_knight_hacks_hacker_discord_role_grant_id_fk": {
+ "name": "knight_hacks_hacker_discord_role_grant_attempt_grant_id_knight_hacks_hacker_discord_role_grant_id_fk",
+ "tableFrom": "knight_hacks_hacker_discord_role_grant_attempt",
+ "tableTo": "knight_hacks_hacker_discord_role_grant",
+ "columnsFrom": ["grant_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_discord_role_grant_attempt_attempted_by_auth_user_id_fk": {
+ "name": "knight_hacks_hacker_discord_role_grant_attempt_attempted_by_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hacker_discord_role_grant_attempt",
+ "tableTo": "auth_user",
+ "columnsFrom": ["attempted_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hacker_discord_role_grant_attempt_attemptToken_unique": {
+ "name": "knight_hacks_hacker_discord_role_grant_attempt_attemptToken_unique",
+ "nullsNotDistinct": false,
+ "columns": ["attempt_token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hacker_discord_role_grant_attempt_role_id_check": {
+ "name": "knight_hacks_hacker_discord_role_grant_attempt_role_id_check",
+ "value": "\"knight_hacks_hacker_discord_role_grant_attempt\".\"role_id_snapshot\" ~ '^[0-9]{17,20}$'"
+ },
+ "knight_hacks_hacker_discord_role_grant_attempt_user_id_check": {
+ "name": "knight_hacks_hacker_discord_role_grant_attempt_user_id_check",
+ "value": "\"knight_hacks_hacker_discord_role_grant_attempt\".\"discord_user_id_snapshot\" ~ '^[0-9]{17,20}$'"
+ },
+ "knight_hacks_hacker_discord_role_grant_attempt_completion_check": {
+ "name": "knight_hacks_hacker_discord_role_grant_attempt_completion_check",
+ "value": "(\"knight_hacks_hacker_discord_role_grant_attempt\".\"outcome\" = 'pending' AND \"knight_hacks_hacker_discord_role_grant_attempt\".\"finished_at\" IS NULL) OR (\"knight_hacks_hacker_discord_role_grant_attempt\".\"outcome\" <> 'pending' AND \"knight_hacks_hacker_discord_role_grant_attempt\".\"finished_at\" IS NOT NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hacker_event_attendee": {
+ "name": "knight_hacks_hacker_event_attendee",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "hacker_att_id": {
+ "name": "hacker_att_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "checked_in_at": {
+ "name": "checked_in_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "checked_in_by": {
+ "name": "checked_in_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "points_awarded": {
+ "name": "points_awarded",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_initial_attendance": {
+ "name": "is_initial_attendance",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "voided_at": {
+ "name": "voided_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "voided_by": {
+ "name": "voided_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "void_reason": {
+ "name": "void_reason",
+ "type": "varchar(300)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "knight_hacks_hacker_event_attendee_event_attendee_history_idx": {
+ "name": "knight_hacks_hacker_event_attendee_event_attendee_history_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "hacker_att_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "checked_in_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_event_attendee_attendee_event_history_idx": {
+ "name": "knight_hacks_hacker_event_attendee_attendee_event_history_idx",
+ "columns": [
+ {
+ "expression": "hacker_att_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "checked_in_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_event_attendee_hackathon_history_idx": {
+ "name": "knight_hacks_hacker_event_attendee_hackathon_history_idx",
+ "columns": [
+ {
+ "expression": "hackathon_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "checked_in_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_event_attendee_checked_in_by_idx": {
+ "name": "knight_hacks_hacker_event_attendee_checked_in_by_idx",
+ "columns": [
+ {
+ "expression": "checked_in_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_event_attendee_voided_by_idx": {
+ "name": "knight_hacks_hacker_event_attendee_voided_by_idx",
+ "columns": [
+ {
+ "expression": "voided_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "knight_hacks_hacker_event_attendee_one_active_initial": {
+ "name": "knight_hacks_hacker_event_attendee_one_active_initial",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "hacker_att_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knight_hacks_hacker_event_attendee\".\"is_initial_attendance\" = true AND \"knight_hacks_hacker_event_attendee\".\"voided_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hacker_event_attendee_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_hacker_event_attendee_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_hacker_event_attendee",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_event_attendee_checked_in_by_auth_user_id_fk": {
+ "name": "knight_hacks_hacker_event_attendee_checked_in_by_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hacker_event_attendee",
+ "tableTo": "auth_user",
+ "columnsFrom": ["checked_in_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_event_attendee_voided_by_auth_user_id_fk": {
+ "name": "knight_hacks_hacker_event_attendee_voided_by_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hacker_event_attendee",
+ "tableTo": "auth_user",
+ "columnsFrom": ["voided_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_event_attendee_scoped_attendee_fk": {
+ "name": "knight_hacks_hacker_event_attendee_scoped_attendee_fk",
+ "tableFrom": "knight_hacks_hacker_event_attendee",
+ "tableTo": "knight_hacks_hacker_attendee",
+ "columnsFrom": ["hacker_att_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_event_attendee_scoped_event_fk": {
+ "name": "knight_hacks_hacker_event_attendee_scoped_event_fk",
+ "tableFrom": "knight_hacks_hacker_event_attendee",
+ "tableTo": "knight_hacks_event",
+ "columnsFrom": ["event_id", "hackathon_id"],
+ "columnsTo": ["id", "hackathon_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hacker_event_attendee_attempt_scope_unique": {
+ "name": "knight_hacks_hacker_event_attendee_attempt_scope_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "hackathon_id", "event_id", "hacker_att_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hacker_event_attendee_points_awarded_check": {
+ "name": "knight_hacks_hacker_event_attendee_points_awarded_check",
+ "value": "\"knight_hacks_hacker_event_attendee\".\"points_awarded\" IS NULL OR \"knight_hacks_hacker_event_attendee\".\"points_awarded\" >= 0"
+ },
+ "knight_hacks_hacker_event_attendee_void_metadata_check": {
+ "name": "knight_hacks_hacker_event_attendee_void_metadata_check",
+ "value": "(\"knight_hacks_hacker_event_attendee\".\"voided_at\" IS NULL AND \"knight_hacks_hacker_event_attendee\".\"void_reason\" IS NULL) OR (\"knight_hacks_hacker_event_attendee\".\"voided_at\" IS NOT NULL AND \"knight_hacks_hacker_event_attendee\".\"void_reason\" IS NOT NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hacker_participant_command": {
+ "name": "knight_hacks_hacker_participant_command",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "operation": {
+ "name": "operation",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "idempotency_key": {
+ "name": "idempotency_key",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload_hash": {
+ "name": "payload_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "hacker_participant_command_state",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'started'"
+ },
+ "result": {
+ "name": "result",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "safe_error_code": {
+ "name": "safe_error_code",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "knight_hacks_hacker_participant_command_expiry_idx": {
+ "name": "knight_hacks_hacker_participant_command_expiry_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hacker_participant_command_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_hacker_participant_command_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hacker_participant_command",
+ "tableTo": "auth_user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_participant_command_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_hacker_participant_command_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_hacker_participant_command",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hacker_participant_command_identity_unique": {
+ "name": "knight_hacks_hacker_participant_command_identity_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "hackathon_id", "operation", "idempotency_key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hacker_participant_command_payload_hash_check": {
+ "name": "knight_hacks_hacker_participant_command_payload_hash_check",
+ "value": "\"knight_hacks_hacker_participant_command\".\"payload_hash\" ~ '^[0-9a-f]{64}$'"
+ },
+ "knight_hacks_hacker_participant_command_completion_check": {
+ "name": "knight_hacks_hacker_participant_command_completion_check",
+ "value": "(\"knight_hacks_hacker_participant_command\".\"state\" = 'started' AND \"knight_hacks_hacker_participant_command\".\"completed_at\" IS NULL) OR (\"knight_hacks_hacker_participant_command\".\"state\" <> 'started' AND \"knight_hacks_hacker_participant_command\".\"completed_at\" IS NOT NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hacker_profile": {
+ "name": "knight_hacks_hacker_profile",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "gender": {
+ "name": "gender",
+ "type": "gender",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "discord_user": {
+ "name": "discord_user",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "country": {
+ "name": "country",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "phone_number": {
+ "name": "phone_number",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "school": {
+ "name": "school",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "level_of_study": {
+ "name": "level_of_study",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "major": {
+ "name": "major",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "race_or_ethnicity": {
+ "name": "race_or_ethnicity",
+ "type": "race_or_ethnicity",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "shirt_size": {
+ "name": "shirt_size",
+ "type": "shirt_size",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "github_profile_url": {
+ "name": "github_profile_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "linkedin_profile_url": {
+ "name": "linkedin_profile_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "website_url": {
+ "name": "website_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resume_url": {
+ "name": "resume_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dob": {
+ "name": "dob",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "grad_date": {
+ "name": "grad_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "food_allergies": {
+ "name": "food_allergies",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_hacker_profile_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_hacker_profile_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hacker_profile",
+ "tableTo": "auth_user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hacker_profile_user_unique": {
+ "name": "knight_hacks_hacker_profile_user_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hacker_profile_revision_check": {
+ "name": "knight_hacks_hacker_profile_revision_check",
+ "value": "\"knight_hacks_hacker_profile\".\"revision\" >= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_hacker_profile_revision": {
+ "name": "knight_hacks_hacker_profile_revision",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "profile_id": {
+ "name": "profile_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "legacy_hacker_id": {
+ "name": "legacy_hacker_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "gender": {
+ "name": "gender",
+ "type": "gender",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "discord_user": {
+ "name": "discord_user",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "country": {
+ "name": "country",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "phone_number": {
+ "name": "phone_number",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "school": {
+ "name": "school",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "level_of_study": {
+ "name": "level_of_study",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "major": {
+ "name": "major",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "race_or_ethnicity": {
+ "name": "race_or_ethnicity",
+ "type": "race_or_ethnicity",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "shirt_size": {
+ "name": "shirt_size",
+ "type": "shirt_size",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "github_profile_url": {
+ "name": "github_profile_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "linkedin_profile_url": {
+ "name": "linkedin_profile_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "website_url": {
+ "name": "website_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resume_url": {
+ "name": "resume_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dob": {
+ "name": "dob",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "grad_date": {
+ "name": "grad_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "food_allergies": {
+ "name": "food_allergies",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "knight_hacks_hacker_profile_revision_profile_created_idx": {
+ "name": "knight_hacks_hacker_profile_revision_profile_created_idx",
+ "columns": [
+ {
+ "expression": "profile_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_hacker_profile_revision_profile_id_knight_hacks_hacker_profile_id_fk": {
+ "name": "knight_hacks_hacker_profile_revision_profile_id_knight_hacks_hacker_profile_id_fk",
+ "tableFrom": "knight_hacks_hacker_profile_revision",
+ "tableTo": "knight_hacks_hacker_profile",
+ "columnsFrom": ["profile_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_profile_revision_legacy_hacker_id_knight_hacks_hacker_id_fk": {
+ "name": "knight_hacks_hacker_profile_revision_legacy_hacker_id_knight_hacks_hacker_id_fk",
+ "tableFrom": "knight_hacks_hacker_profile_revision",
+ "tableTo": "knight_hacks_hacker",
+ "columnsFrom": ["legacy_hacker_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_hacker_profile_revision_created_by_auth_user_id_fk": {
+ "name": "knight_hacks_hacker_profile_revision_created_by_auth_user_id_fk",
+ "tableFrom": "knight_hacks_hacker_profile_revision",
+ "tableTo": "auth_user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_hacker_profile_revision_legacyHackerId_unique": {
+ "name": "knight_hacks_hacker_profile_revision_legacyHackerId_unique",
+ "nullsNotDistinct": false,
+ "columns": ["legacy_hacker_id"]
+ },
+ "knight_hacks_hacker_profile_revision_profile_version_unique": {
+ "name": "knight_hacks_hacker_profile_revision_profile_version_unique",
+ "nullsNotDistinct": false,
+ "columns": ["profile_id", "revision"]
+ },
+ "knight_hacks_hacker_profile_revision_id_profile_unique": {
+ "name": "knight_hacks_hacker_profile_revision_id_profile_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "profile_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_hacker_profile_revision_version_check": {
+ "name": "knight_hacks_hacker_profile_revision_version_check",
+ "value": "\"knight_hacks_hacker_profile_revision\".\"revision\" >= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_issue": {
+ "name": "knight_hacks_issue",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "status": {
+ "name": "status",
+ "type": "issue_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "links": {
+ "name": "links",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "event": {
+ "name": "event",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discord_thread_id": {
+ "name": "discord_thread_id",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date": {
+ "name": "date",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "due_at": {
+ "name": "due_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "issue_priority",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "team": {
+ "name": "team",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "creator": {
+ "name": "creator",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent": {
+ "name": "parent",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_by": {
+ "name": "archived_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archive_batch_id": {
+ "name": "archive_batch_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "creation_key": {
+ "name": "creation_key",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "creation_hash": {
+ "name": "creation_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "issue_team_idx": {
+ "name": "issue_team_idx",
+ "columns": [
+ {
+ "expression": "team",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "issue_creator_idx": {
+ "name": "issue_creator_idx",
+ "columns": [
+ {
+ "expression": "creator",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "issue_status_idx": {
+ "name": "issue_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "issue_date_idx": {
+ "name": "issue_date_idx",
+ "columns": [
+ {
+ "expression": "date",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "issue_due_at_idx": {
+ "name": "issue_due_at_idx",
+ "columns": [
+ {
+ "expression": "due_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "issue_archive_idx": {
+ "name": "issue_archive_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "archive_batch_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "issue_parent_idx": {
+ "name": "issue_parent_idx",
+ "columns": [
+ {
+ "expression": "parent",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "issue_priority_idx": {
+ "name": "issue_priority_idx",
+ "columns": [
+ {
+ "expression": "priority",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_issue_event_knight_hacks_event_id_fk": {
+ "name": "knight_hacks_issue_event_knight_hacks_event_id_fk",
+ "tableFrom": "knight_hacks_issue",
+ "tableTo": "knight_hacks_event",
+ "columnsFrom": ["event"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_issue_team_auth_roles_id_fk": {
+ "name": "knight_hacks_issue_team_auth_roles_id_fk",
+ "tableFrom": "knight_hacks_issue",
+ "tableTo": "auth_roles",
+ "columnsFrom": ["team"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_issue_creator_auth_user_id_fk": {
+ "name": "knight_hacks_issue_creator_auth_user_id_fk",
+ "tableFrom": "knight_hacks_issue",
+ "tableTo": "auth_user",
+ "columnsFrom": ["creator"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_issue_archived_by_auth_user_id_fk": {
+ "name": "knight_hacks_issue_archived_by_auth_user_id_fk",
+ "tableFrom": "knight_hacks_issue",
+ "tableTo": "auth_user",
+ "columnsFrom": ["archived_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "issue_parent_fk": {
+ "name": "issue_parent_fk",
+ "tableFrom": "knight_hacks_issue",
+ "tableTo": "knight_hacks_issue",
+ "columnsFrom": ["parent"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_issue_creation_key_unique": {
+ "name": "knight_hacks_issue_creation_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["creation_key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_issue_history": {
+ "name": "knight_hacks_issue_history",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "issue_id": {
+ "name": "issue_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_display_name": {
+ "name": "actor_display_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "changed_fields": {
+ "name": "changed_fields",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "before": {
+ "name": "before",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "after": {
+ "name": "after",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "issue_history_issue_created_idx": {
+ "name": "issue_history_issue_created_idx",
+ "columns": [
+ {
+ "expression": "issue_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_issue_history_issue_id_knight_hacks_issue_id_fk": {
+ "name": "knight_hacks_issue_history_issue_id_knight_hacks_issue_id_fk",
+ "tableFrom": "knight_hacks_issue_history",
+ "tableTo": "knight_hacks_issue",
+ "columnsFrom": ["issue_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_issue_history_actor_id_auth_user_id_fk": {
+ "name": "knight_hacks_issue_history_actor_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_issue_history",
+ "tableTo": "auth_user",
+ "columnsFrom": ["actor_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_issue_reminder_delivery": {
+ "name": "knight_hacks_issue_reminder_delivery",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "issue_id": {
+ "name": "issue_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "due_at": {
+ "name": "due_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reminder_key": {
+ "name": "reminder_key",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destination_snapshot": {
+ "name": "destination_snapshot",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_snapshot": {
+ "name": "content_snapshot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(24)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locked_at": {
+ "name": "locked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivered_at": {
+ "name": "delivered_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "issue_reminder_delivery_pending_idx": {
+ "name": "issue_reminder_delivery_pending_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knight_hacks_issue_reminder_delivery_issue_id_knight_hacks_issue_id_fk": {
+ "name": "knight_hacks_issue_reminder_delivery_issue_id_knight_hacks_issue_id_fk",
+ "tableFrom": "knight_hacks_issue_reminder_delivery",
+ "tableTo": "knight_hacks_issue",
+ "columnsFrom": ["issue_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "issue_reminder_delivery_identity_unique": {
+ "name": "issue_reminder_delivery_identity_unique",
+ "nullsNotDistinct": false,
+ "columns": ["issue_id", "due_at", "reminder_key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_issues_to_teams_visibility": {
+ "name": "knight_hacks_issues_to_teams_visibility",
+ "schema": "",
+ "columns": {
+ "issue_id": {
+ "name": "issue_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "team_id": {
+ "name": "team_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_issues_to_teams_visibility_issue_id_knight_hacks_issue_id_fk": {
+ "name": "knight_hacks_issues_to_teams_visibility_issue_id_knight_hacks_issue_id_fk",
+ "tableFrom": "knight_hacks_issues_to_teams_visibility",
+ "tableTo": "knight_hacks_issue",
+ "columnsFrom": ["issue_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_issues_to_teams_visibility_team_id_auth_roles_id_fk": {
+ "name": "knight_hacks_issues_to_teams_visibility_team_id_auth_roles_id_fk",
+ "tableFrom": "knight_hacks_issues_to_teams_visibility",
+ "tableTo": "auth_roles",
+ "columnsFrom": ["team_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "knight_hacks_issues_to_teams_visibility_issue_id_team_id_pk": {
+ "name": "knight_hacks_issues_to_teams_visibility_issue_id_team_id_pk",
+ "columns": ["issue_id", "team_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_issues_to_users_assignment": {
+ "name": "knight_hacks_issues_to_users_assignment",
+ "schema": "",
+ "columns": {
+ "issue_id": {
+ "name": "issue_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_issues_to_users_assignment_issue_id_knight_hacks_issue_id_fk": {
+ "name": "knight_hacks_issues_to_users_assignment_issue_id_knight_hacks_issue_id_fk",
+ "tableFrom": "knight_hacks_issues_to_users_assignment",
+ "tableTo": "knight_hacks_issue",
+ "columnsFrom": ["issue_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_issues_to_users_assignment_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_issues_to_users_assignment_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_issues_to_users_assignment",
+ "tableTo": "auth_user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "knight_hacks_issues_to_users_assignment_issue_id_user_id_pk": {
+ "name": "knight_hacks_issues_to_users_assignment_issue_id_user_id_pk",
+ "columns": ["issue_id", "user_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_judged_submission": {
+ "name": "knight_hacks_judged_submission",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "submission_id": {
+ "name": "submission_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "judge_id": {
+ "name": "judge_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "private_feedback": {
+ "name": "private_feedback",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "public_feedback": {
+ "name": "public_feedback",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "originality_rating": {
+ "name": "originality_rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "design_rating": {
+ "name": "design_rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "technical_understanding_rating": {
+ "name": "technical_understanding_rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "implementation_rating": {
+ "name": "implementation_rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "wow_factor_rating": {
+ "name": "wow_factor_rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_judged_submission_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_judged_submission_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_judged_submission",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_judged_submission_submission_id_knight_hacks_submissions_id_fk": {
+ "name": "knight_hacks_judged_submission_submission_id_knight_hacks_submissions_id_fk",
+ "tableFrom": "knight_hacks_judged_submission",
+ "tableTo": "knight_hacks_submissions",
+ "columnsFrom": ["submission_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_judged_submission_judge_id_knight_hacks_judges_id_fk": {
+ "name": "knight_hacks_judged_submission_judge_id_knight_hacks_judges_id_fk",
+ "tableFrom": "knight_hacks_judged_submission",
+ "tableTo": "knight_hacks_judges",
+ "columnsFrom": ["judge_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_judges": {
+ "name": "knight_hacks_judges",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "room_name": {
+ "name": "room_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "challenge_id": {
+ "name": "challenge_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_judges_challenge_id_knight_hacks_challenges_id_fk": {
+ "name": "knight_hacks_judges_challenge_id_knight_hacks_challenges_id_fk",
+ "tableFrom": "knight_hacks_judges",
+ "tableTo": "knight_hacks_challenges",
+ "columnsFrom": ["challenge_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_member": {
+ "name": "knight_hacks_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "discord_user": {
+ "name": "discord_user",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "age": {
+ "name": "age",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "phone_number": {
+ "name": "phone_number",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "school": {
+ "name": "school",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "level_of_study": {
+ "name": "level_of_study",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "major": {
+ "name": "major",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Computer Science'"
+ },
+ "gender": {
+ "name": "gender",
+ "type": "gender",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Prefer not to answer'"
+ },
+ "race_or_ethnicity": {
+ "name": "race_or_ethnicity",
+ "type": "race_or_ethnicity",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Prefer not to answer'"
+ },
+ "guild_profile_visible": {
+ "name": "guild_profile_visible",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "guild_resume_visible": {
+ "name": "guild_resume_visible",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "guild_opportunity_statuses": {
+ "name": "guild_opportunity_statuses",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "tagline": {
+ "name": "tagline",
+ "type": "varchar(80)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "about": {
+ "name": "about",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "profile_picture_url": {
+ "name": "profile_picture_url",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "shirt_size": {
+ "name": "shirt_size",
+ "type": "shirt_size",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "github_profile_url": {
+ "name": "github_profile_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "linkedin_profile_url": {
+ "name": "linkedin_profile_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "website_url": {
+ "name": "website_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resume_url": {
+ "name": "resume_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dob": {
+ "name": "dob",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "grad_date": {
+ "name": "grad_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "alumni_confirmed_at": {
+ "name": "alumni_confirmed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "company": {
+ "name": "company",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_city_key": {
+ "name": "current_city_key",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "guild_location_visible": {
+ "name": "guild_location_visible",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "points": {
+ "name": "points",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "date_created": {
+ "name": "date_created",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "time",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_member_user_id_auth_user_id_fk": {
+ "name": "knight_hacks_member_user_id_auth_user_id_fk",
+ "tableFrom": "knight_hacks_member",
+ "tableTo": "auth_user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_member_email_unique": {
+ "name": "knight_hacks_member_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ },
+ "knight_hacks_member_phoneNumber_unique": {
+ "name": "knight_hacks_member_phoneNumber_unique",
+ "nullsNotDistinct": false,
+ "columns": ["phone_number"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "knight_hacks_member_valid_guild_opportunity_statuses": {
+ "name": "knight_hacks_member_valid_guild_opportunity_statuses",
+ "value": "\"knight_hacks_member\".\"guild_opportunity_statuses\" <@ ARRAY['internships', 'full-time', 'freelance-contract', 'project-collaboration', 'offering-mentorship', 'seeking-mentorship']::text[] AND cardinality(\"knight_hacks_member\".\"guild_opportunity_statuses\") <= 3"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_companies": {
+ "name": "knight_hacks_companies",
+ "schema": "",
+ "columns": {
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": true,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_sponsor": {
+ "name": "knight_hacks_sponsor",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "logo_url": {
+ "name": "logo_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "website_url": {
+ "name": "website_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_submissions": {
+ "name": "knight_hacks_submissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "challenge_id": {
+ "name": "challenge_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "team_id": {
+ "name": "team_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_submissions_challenge_id_knight_hacks_challenges_id_fk": {
+ "name": "knight_hacks_submissions_challenge_id_knight_hacks_challenges_id_fk",
+ "tableFrom": "knight_hacks_submissions",
+ "tableTo": "knight_hacks_challenges",
+ "columnsFrom": ["challenge_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_submissions_team_id_knight_hacks_teams_id_fk": {
+ "name": "knight_hacks_submissions_team_id_knight_hacks_teams_id_fk",
+ "tableFrom": "knight_hacks_submissions",
+ "tableTo": "knight_hacks_teams",
+ "columnsFrom": ["team_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knight_hacks_submissions_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_submissions_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_submissions",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_submissions_teamId_challengeId_unique": {
+ "name": "knight_hacks_submissions_teamId_challengeId_unique",
+ "nullsNotDistinct": false,
+ "columns": ["team_id", "challenge_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_teams": {
+ "name": "knight_hacks_teams",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "hackathon_id": {
+ "name": "hackathon_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_title": {
+ "name": "project_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "submission_url": {
+ "name": "submission_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_created_at": {
+ "name": "project_created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_project_submitted": {
+ "name": "is_project_submitted",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "devpost_url": {
+ "name": "devpost_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notes": {
+ "name": "notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "universities": {
+ "name": "universities",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "emails": {
+ "name": "emails",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "match_key": {
+ "name": "match_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_teams_hackathon_id_knight_hacks_hackathon_id_fk": {
+ "name": "knight_hacks_teams_hackathon_id_knight_hacks_hackathon_id_fk",
+ "tableFrom": "knight_hacks_teams",
+ "tableTo": "knight_hacks_hackathon",
+ "columnsFrom": ["hackathon_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_teams_matchKey_unique": {
+ "name": "knight_hacks_teams_matchKey_unique",
+ "nullsNotDistinct": false,
+ "columns": ["match_key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_template": {
+ "name": "knight_hacks_template",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_name": {
+ "name": "normalized_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "body": {
+ "name": "body",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "disabled_at": {
+ "name": "disabled_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disabled_reason": {
+ "name": "disabled_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "knight_hacks_template_normalized_name_unique": {
+ "name": "knight_hacks_template_normalized_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["normalized_name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knight_hacks_trpc_form_connection": {
+ "name": "knight_hacks_trpc_form_connection",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "form": {
+ "name": "form",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "proc": {
+ "name": "proc",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connections": {
+ "name": "connections",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "knight_hacks_trpc_form_connection_form_knight_hacks_form_schemas_id_fk": {
+ "name": "knight_hacks_trpc_form_connection_form_knight_hacks_form_schemas_id_fk",
+ "tableFrom": "knight_hacks_trpc_form_connection",
+ "tableTo": "knight_hacks_form_schemas",
+ "columnsFrom": ["form"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.alumni_bulletin_state": {
+ "name": "alumni_bulletin_state",
+ "schema": "public",
+ "values": ["draft", "published", "archived"]
+ },
+ "public.company_review_state": {
+ "name": "company_review_state",
+ "schema": "public",
+ "values": ["pending", "approved", "rejected", "merged"]
+ },
+ "public.employment_experience_type": {
+ "name": "employment_experience_type",
+ "schema": "public",
+ "values": [
+ "internship",
+ "full_time",
+ "part_time",
+ "co_op",
+ "contract",
+ "fellowship",
+ "self_employed",
+ "other"
+ ]
+ },
+ "public.employment_state": {
+ "name": "employment_state",
+ "schema": "public",
+ "values": ["current", "past", "unknown"]
+ },
+ "public.event_discord_entity_type": {
+ "name": "event_discord_entity_type",
+ "schema": "public",
+ "values": ["external", "voice", "stage"]
+ },
+ "public.event_google_destination": {
+ "name": "event_google_destination",
+ "schema": "public",
+ "values": ["public", "internal"]
+ },
+ "public.event_publication_work_state": {
+ "name": "event_publication_work_state",
+ "schema": "public",
+ "values": ["pending", "processing", "succeeded", "failed", "blocked"]
+ },
+ "public.event_purpose": {
+ "name": "event_purpose",
+ "schema": "public",
+ "values": ["event", "primary_check_in"]
+ },
+ "public.event_sync_state": {
+ "name": "event_sync_state",
+ "schema": "public",
+ "values": ["disabled", "pending", "synced", "error", "unknown"]
+ },
+ "public.form_callback_status": {
+ "name": "form_callback_status",
+ "schema": "public",
+ "values": ["pending", "running", "succeeded", "failed", "cancelled"]
+ },
+ "public.form_kind": {
+ "name": "form_kind",
+ "schema": "public",
+ "values": ["general", "event_feedback", "system"]
+ },
+ "public.form_response_mode": {
+ "name": "form_response_mode",
+ "schema": "public",
+ "values": ["single_locked", "single_editable", "multiple_locked"]
+ },
+ "public.form_state": {
+ "name": "form_state",
+ "schema": "public",
+ "values": ["draft", "published", "archived"]
+ },
+ "public.gender": {
+ "name": "gender",
+ "schema": "public",
+ "values": [
+ "Man",
+ "Woman",
+ "Non-binary",
+ "Prefer to self-describe",
+ "Prefer not to answer"
+ ]
+ },
+ "public.hackathon_application_state": {
+ "name": "hackathon_application_state",
+ "schema": "public",
+ "values": [
+ "withdrawn",
+ "pending",
+ "accepted",
+ "waitlisted",
+ "checkedin",
+ "confirmed",
+ "denied"
+ ]
+ },
+ "public.hackathon_event_publication_provider": {
+ "name": "hackathon_event_publication_provider",
+ "schema": "public",
+ "values": ["discord", "google"]
+ },
+ "public.hackathon_event_reminder_state": {
+ "name": "hackathon_event_reminder_state",
+ "schema": "public",
+ "values": ["pending", "delivering", "delivered", "failed", "unknown"]
+ },
+ "public.hacker_agreement_provenance": {
+ "name": "hacker_agreement_provenance",
+ "schema": "public",
+ "values": ["explicit", "legacy_unversioned"]
+ },
+ "public.hacker_agreement_stage": {
+ "name": "hacker_agreement_stage",
+ "schema": "public",
+ "values": ["application", "confirmation"]
+ },
+ "public.hacker_check_in_outcome": {
+ "name": "hacker_check_in_outcome",
+ "schema": "public",
+ "values": [
+ "checked_in",
+ "already_checked_in",
+ "invalid_qr",
+ "hacker_not_found",
+ "wrong_status",
+ "not_checked_in",
+ "wrong_class",
+ "not_ready"
+ ]
+ },
+ "public.hacker_discord_role_attempt_outcome": {
+ "name": "hacker_discord_role_attempt_outcome",
+ "schema": "public",
+ "values": ["pending", "succeeded", "failed", "unknown"]
+ },
+ "public.hacker_discord_role_grant_state": {
+ "name": "hacker_discord_role_grant_state",
+ "schema": "public",
+ "values": ["pending", "succeeded", "failed", "unknown"]
+ },
+ "public.hacker_discord_role_kind": {
+ "name": "hacker_discord_role_kind",
+ "schema": "public",
+ "values": ["general", "class", "vip"]
+ },
+ "public.hacker_participant_command_state": {
+ "name": "hacker_participant_command_state",
+ "schema": "public",
+ "values": ["started", "completed"]
+ },
+ "public.issue_priority": {
+ "name": "issue_priority",
+ "schema": "public",
+ "values": ["Lowest", "Low", "Medium", "High", "Highest"]
+ },
+ "public.issue_status": {
+ "name": "issue_status",
+ "schema": "public",
+ "values": ["Backlog", "Planning", "In Progress", "Finished"]
+ },
+ "public.race_or_ethnicity": {
+ "name": "race_or_ethnicity",
+ "schema": "public",
+ "values": [
+ "White",
+ "Black or African American",
+ "Hispanic / Latino / Spanish Origin",
+ "Asian",
+ "Native Hawaiian or Other Pacific Islander",
+ "Native American or Alaskan Native",
+ "Middle Eastern",
+ "Prefer not to answer",
+ "Other"
+ ]
+ },
+ "public.shirt_size": {
+ "name": "shirt_size",
+ "schema": "public",
+ "values": ["XS", "S", "M", "L", "XL", "2XL", "3XL"]
+ },
+ "public.sponsor_tier": {
+ "name": "sponsor_tier",
+ "schema": "public",
+ "values": ["gold", "silver", "bronze", "other"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json
index 520225f77..d5d3c980f 100644
--- a/packages/db/drizzle/meta/_journal.json
+++ b/packages/db/drizzle/meta/_journal.json
@@ -288,6 +288,13 @@
"when": 1786501513660,
"tag": "0040_gigantic_blockbuster",
"breakpoints": true
+ },
+ {
+ "idx": 41,
+ "version": "7",
+ "when": 1786667296492,
+ "tag": "0041_lying_jackal",
+ "breakpoints": true
}
]
}
diff --git a/packages/db/scripts/dev-db-backup-sanitizer.ts b/packages/db/scripts/dev-db-backup-sanitizer.ts
index 2bf9afcc8..fa43358e2 100644
--- a/packages/db/scripts/dev-db-backup-sanitizer.ts
+++ b/packages/db/scripts/dev-db-backup-sanitizer.ts
@@ -24,6 +24,7 @@ export const TABLES_TO_KEEP = [
"knight_hacks_company",
"knight_hacks_discord_config",
"knight_hacks_dues_configuration",
+ "knight_hacks_dues_entitlement",
"knight_hacks_dues_payment",
"knight_hacks_employment",
"knight_hacks_event",
@@ -95,6 +96,9 @@ export const TABLES_TO_DROP = [
"email_send",
"email_send_event",
"email_send_recipient",
+ // Live Stripe checkout coordination. Development must start without a
+ // production member's reserved PaymentIntent.
+ "knight_hacks_dues_checkout_reservation",
// Uploaded objects are not copied with this data-only export. Response files
// are private, and retained form definitions have object-backed instruction
// references removed below so they do not point at missing MinIO content.
diff --git a/packages/db/src/schemas/knight-hacks.ts b/packages/db/src/schemas/knight-hacks.ts
index deb407f1f..59ffb362e 100644
--- a/packages/db/src/schemas/knight-hacks.ts
+++ b/packages/db/src/schemas/knight-hacks.ts
@@ -2027,13 +2027,11 @@ export const DuesPayment = createTable(
amount: t.integer().notNull(),
paymentDate: t.timestamp().notNull(),
year: t.integer().notNull(),
- active: t.boolean().notNull().default(true),
stripePaymentIntentId: t.varchar("stripe_payment_intent_id", {
length: 255,
}),
}),
(table) => ({
- uniqueMemberYear: unique().on(table.memberId, table.year),
uniqueStripePaymentIntent: unique(
"knight_hacks_dues_payment_stripe_payment_intent_id_unique",
).on(table.stripePaymentIntentId),
@@ -2045,6 +2043,68 @@ export type SelectDuesPayment = typeof DuesPayment.$inferSelect;
export const DuesPaymentSchema = createInsertSchema(DuesPayment);
+export const DuesEntitlement = createTable(
+ "dues_entitlement",
+ (t) => ({
+ id: t.uuid().notNull().primaryKey().defaultRandom(),
+ memberId: t
+ .uuid()
+ .notNull()
+ .references(() => Member.id, {
+ onDelete: "cascade",
+ }),
+ year: t.integer().notNull(),
+ active: t.boolean().notNull().default(true),
+ sourcePaymentId: t
+ .uuid("source_payment_id")
+ .references(() => DuesPayment.id, { onDelete: "set null" }),
+ createdAt: t.timestamp().notNull().defaultNow(),
+ updatedAt: t.timestamp().notNull().defaultNow(),
+ }),
+ (table) => ({
+ uniqueMemberYear: unique().on(table.memberId, table.year),
+ uniqueSourcePayment: unique().on(table.sourcePaymentId),
+ }),
+);
+
+export type InsertDuesEntitlement = typeof DuesEntitlement.$inferInsert;
+export type SelectDuesEntitlement = typeof DuesEntitlement.$inferSelect;
+
+export const DuesEntitlementSchema = createInsertSchema(DuesEntitlement);
+
+export const DuesCheckoutReservation = createTable(
+ "dues_checkout_reservation",
+ (t) => ({
+ id: t.uuid().notNull().primaryKey().defaultRandom(),
+ memberId: t
+ .uuid()
+ .notNull()
+ .references(() => Member.id, {
+ onDelete: "cascade",
+ }),
+ year: t.integer().notNull(),
+ attemptNumber: t.integer().notNull().default(0),
+ stripePaymentIntentId: t.varchar("stripe_payment_intent_id", {
+ length: 255,
+ }),
+ createdAt: t.timestamp().notNull().defaultNow(),
+ updatedAt: t.timestamp().notNull().defaultNow(),
+ }),
+ (table) => ({
+ uniqueMemberYear: unique().on(table.memberId, table.year),
+ uniqueStripePaymentIntent: unique().on(table.stripePaymentIntentId),
+ }),
+);
+
+export type InsertDuesCheckoutReservation =
+ typeof DuesCheckoutReservation.$inferInsert;
+export type SelectDuesCheckoutReservation =
+ typeof DuesCheckoutReservation.$inferSelect;
+
+export const DuesCheckoutReservationSchema = createInsertSchema(
+ DuesCheckoutReservation,
+);
+
export const EventFeedback = createTable("event_feedback", (t) => ({
id: t.uuid().notNull().primaryKey().defaultRandom(),
memberId: t
diff --git a/packages/db/src/tests/dev-db-backup-sanitizer.test.ts b/packages/db/src/tests/dev-db-backup-sanitizer.test.ts
index c9d40532d..d62fb985f 100644
--- a/packages/db/src/tests/dev-db-backup-sanitizer.test.ts
+++ b/packages/db/src/tests/dev-db-backup-sanitizer.test.ts
@@ -86,6 +86,7 @@ describe("development database backup sanitizer", () => {
"knight_hacks_club_team_role",
"knight_hacks_discord_config",
"knight_hacks_dues_configuration",
+ "knight_hacks_dues_entitlement",
]),
);
});
diff --git a/packages/db/src/tests/dues-entitlement-migration.test.ts b/packages/db/src/tests/dues-entitlement-migration.test.ts
new file mode 100644
index 000000000..4ef5edd17
--- /dev/null
+++ b/packages/db/src/tests/dues-entitlement-migration.test.ts
@@ -0,0 +1,69 @@
+import { readdir, readFile } from "node:fs/promises";
+import { describe, expect, it } from "vitest";
+
+const migrationsDirectory = new URL("../../drizzle/", import.meta.url);
+
+async function readDuesEntitlementMigration() {
+ const files = (await readdir(migrationsDirectory))
+ .filter((file) => file.endsWith(".sql"))
+ .sort();
+ for (const file of files) {
+ const sql = await readFile(new URL(file, migrationsDirectory), "utf8");
+ if (sql.includes(`CREATE TABLE "knight_hacks_dues_entitlement"`)) {
+ return { file, sql };
+ }
+ }
+ throw new Error("Dues entitlement migration was not found.");
+}
+
+describe("dues entitlement migration contract", () => {
+ it("separates immutable payments from yearly entitlement state", async () => {
+ const { file, sql } = await readDuesEntitlementMigration();
+
+ expect(file.startsWith("0041_")).toBe(true);
+ expect(sql).toContain(`CREATE TABLE "knight_hacks_dues_entitlement"`);
+ expect(sql).toContain(`UNIQUE("member_id","year")`);
+ expect(sql).toContain(`UNIQUE("source_payment_id")`);
+ expect(sql).toContain(`ON DELETE set null`);
+ expect(sql).toContain(
+ `ALTER TABLE "knight_hacks_dues_payment" DROP COLUMN "active"`,
+ );
+ expect(sql).not.toContain(`DROP TABLE "knight_hacks_dues_payment"`);
+ });
+
+ it("normalizes legacy calendar years before backfilling entitlement state", async () => {
+ const { sql } = await readDuesEntitlementMigration();
+ const normalize = sql.indexOf(
+ `UPDATE "knight_hacks_dues_payment"\nSET "year" = "year" - 1`,
+ );
+ const backfill = sql.indexOf(`INSERT INTO "knight_hacks_dues_entitlement"`);
+ const dropActive = sql.indexOf(
+ `ALTER TABLE "knight_hacks_dues_payment" DROP COLUMN "active"`,
+ );
+
+ expect(normalize).toBeGreaterThan(-1);
+ expect(sql).toContain(
+ `WHERE "year" = extract(year FROM "payment_date")::integer`,
+ );
+ expect(sql).toContain(`AND extract(month FROM "payment_date") < 8`);
+ expect(backfill).toBeGreaterThan(normalize);
+ expect(dropActive).toBeGreaterThan(backfill);
+ expect(sql).toContain(`bool_or("active")`);
+ expect(sql).toContain(
+ `array_agg("id" ORDER BY "active" DESC, "payment_date" DESC, "id")`,
+ );
+ });
+
+ it("keeps one reserved PaymentIntent per member and year", async () => {
+ const { file, sql } = await readDuesEntitlementMigration();
+
+ expect(file.startsWith("0041_")).toBe(true);
+ expect(sql).toContain(
+ `CREATE TABLE "knight_hacks_dues_checkout_reservation"`,
+ );
+ expect(sql).toContain(`UNIQUE("member_id","year")`);
+ expect(sql).toContain(`UNIQUE("stripe_payment_intent_id")`);
+ expect(sql).toContain(`"attempt_number" integer DEFAULT 0 NOT NULL`);
+ expect(sql).toContain(`ON DELETE cascade`);
+ });
+});
diff --git a/packages/db/src/tests/migration-lineage.test.ts b/packages/db/src/tests/migration-lineage.test.ts
index f38d0d4db..3311f8e0e 100644
--- a/packages/db/src/tests/migration-lineage.test.ts
+++ b/packages/db/src/tests/migration-lineage.test.ts
@@ -106,15 +106,15 @@ describe("canonical production migration lineage", () => {
await readFile(new URL("_journal.json", metadataDirectory), "utf8"),
) as { entries: JournalEntry[] };
- expect(journal.entries).toHaveLength(41);
+ expect(journal.entries).toHaveLength(42);
expect(journal.entries[10]).toMatchObject({
idx: 10,
tag: "0010_wooden_supreme_intelligence",
when: PRODUCTION_MAIN_MIGRATION.createdAt,
});
expect(journal.entries.at(-1)).toMatchObject({
- idx: 40,
- tag: "0040_gigantic_blockbuster",
+ idx: 41,
+ tag: "0041_lying_jackal",
});
for (const [position, entry] of journal.entries.entries()) {
diff --git a/packages/validators/src/audit.ts b/packages/validators/src/audit.ts
index 6c86839dc..bf24a8f0c 100644
--- a/packages/validators/src/audit.ts
+++ b/packages/validators/src/audit.ts
@@ -1,3 +1,4 @@
+/* eslint-disable max-lines -- The centralized audit catalog is intentionally kept together for reviewability. */
import { z } from "zod";
const policy = <
@@ -193,8 +194,8 @@ export const AUDIT_ACTION_CATALOG = {
"reactivated",
]),
"member.dues.revoked": policy("members", "Revoked member dues", [
- "academicYears",
- "affectedPaymentCount",
+ "academicYear",
+ "affectedEntitlementCount",
]),
"member.dues.invalidated_bulk": policy(
"members",
diff --git a/packages/validators/src/dues.ts b/packages/validators/src/dues.ts
index d5458754d..72361c3e6 100644
--- a/packages/validators/src/dues.ts
+++ b/packages/validators/src/dues.ts
@@ -55,18 +55,6 @@ export function isLateDuesPaymentWindow(referenceDate = new Date()) {
return currentDay >= warningStart && currentDay <= warningEnd;
}
-export function getDuesPayableYear({
- currentAcademicYearStart,
- hasStaleCurrentYearDues,
-}: {
- currentAcademicYearStart: number;
- hasStaleCurrentYearDues: boolean;
-}) {
- return hasStaleCurrentYearDues
- ? currentAcademicYearStart + 1
- : currentAcademicYearStart;
-}
-
export function formatDuesAmount(cents = MEMBER_DUES_PRICE_CENTS) {
return new Intl.NumberFormat("en-US", {
currency: "USD",
diff --git a/packages/validators/src/tests/dues.test.ts b/packages/validators/src/tests/dues.test.ts
index 6cf20aef2..d2dafab56 100644
--- a/packages/validators/src/tests/dues.test.ts
+++ b/packages/validators/src/tests/dues.test.ts
@@ -6,7 +6,6 @@ import {
buildDuesAcademicYear,
formatDuesAmount,
getDuesAcademicYear,
- getDuesPayableYear,
isLateDuesPaymentWindow,
} from "../dues";
@@ -44,21 +43,6 @@ describe("dues academic-year helpers", () => {
);
});
- it("bumps payable year forward when the current-year dues row is stale", () => {
- expect(
- getDuesPayableYear({
- currentAcademicYearStart: 2026,
- hasStaleCurrentYearDues: false,
- }),
- ).toBe(2026);
- expect(
- getDuesPayableYear({
- currentAcademicYearStart: 2026,
- hasStaleCurrentYearDues: true,
- }),
- ).toBe(2027);
- });
-
it("formats dues amounts from cents", () => {
expect(CLUB.DUES_PAYMENT).toBe(2500);
expect(formatDuesAmount(2500)).toBe("$25.00");