@bash-app/bash-common 30.116.0 → 30.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/extendedSchemas.d.ts +54 -0
  2. package/dist/extendedSchemas.d.ts.map +1 -1
  3. package/dist/extendedSchemas.js +10 -1
  4. package/dist/extendedSchemas.js.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/utils/__tests__/paymentUtils.test.d.ts +6 -0
  9. package/dist/utils/__tests__/paymentUtils.test.d.ts.map +1 -0
  10. package/dist/utils/__tests__/paymentUtils.test.js +77 -0
  11. package/dist/utils/__tests__/paymentUtils.test.js.map +1 -0
  12. package/dist/utils/discountEngine/__tests__/bestPriceResolver.test.d.ts +2 -0
  13. package/dist/utils/discountEngine/__tests__/bestPriceResolver.test.d.ts.map +1 -0
  14. package/dist/utils/discountEngine/__tests__/bestPriceResolver.test.js +457 -0
  15. package/dist/utils/discountEngine/__tests__/bestPriceResolver.test.js.map +1 -0
  16. package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.d.ts +2 -0
  17. package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.d.ts.map +1 -0
  18. package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.js +480 -0
  19. package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.js.map +1 -0
  20. package/package.json +2 -2
  21. package/prisma/COMPREHENSIVE-MIGRATION-README.md +295 -0
  22. package/prisma/MIGRATION-FILES-GUIDE.md +76 -0
  23. package/prisma/comprehensive-migration-20260120.sql +5751 -0
  24. package/prisma/delta-migration-20260120.sql +302 -0
  25. package/prisma/schema.prisma +253 -13
  26. package/prisma/verify-migration.sql +132 -0
  27. package/src/extendedSchemas.ts +10 -1
  28. package/src/index.ts +4 -0
  29. package/src/utils/__tests__/paymentUtils.test.ts +95 -0
  30. package/src/utils/discountEngine/__tests__/bestPriceResolver.test.ts +558 -0
  31. package/src/utils/discountEngine/__tests__/eligibilityValidator.test.ts +655 -0
@@ -0,0 +1,302 @@
1
+ -- ============================================================================
2
+ -- DELTA MIGRATION - January 20, 2026
3
+ -- New columns and tables added since last commit
4
+ -- ============================================================================
5
+
6
+ -- 1. BashFeedPost: Make bashId optional, add serviceId
7
+ -- ============================================================================
8
+ ALTER TABLE "BashFeedPost" ALTER COLUMN "bashId" DROP NOT NULL;
9
+ ALTER TABLE "BashFeedPost" ADD COLUMN IF NOT EXISTS "serviceId" TEXT;
10
+ ALTER TABLE "BashFeedPost" ADD CONSTRAINT "BashFeedPost_serviceId_fkey"
11
+ FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
12
+ CREATE INDEX IF NOT EXISTS "BashFeedPost_serviceId_idx" ON "BashFeedPost"("serviceId");
13
+
14
+ -- 2. BashEvent: Add isAutoApprovable and organization fields
15
+ -- ============================================================================
16
+ ALTER TABLE "BashEvent" ADD COLUMN IF NOT EXISTS "isAutoApprovable" BOOLEAN NOT NULL DEFAULT false;
17
+ ALTER TABLE "BashEvent" ADD COLUMN IF NOT EXISTS "organizationId" TEXT;
18
+ ALTER TABLE "BashEvent" ADD CONSTRAINT "BashEvent_organizationId_fkey"
19
+ FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE;
20
+ CREATE INDEX IF NOT EXISTS "BashEvent_organizationId_idx" ON "BashEvent"("organizationId");
21
+
22
+ -- 3. Service: Add verification and certification fields
23
+ -- ============================================================================
24
+ ALTER TABLE "Service" ADD COLUMN IF NOT EXISTS "isVerified" BOOLEAN NOT NULL DEFAULT false;
25
+ ALTER TABLE "Service" ADD COLUMN IF NOT EXISTS "isLicensed" BOOLEAN NOT NULL DEFAULT false;
26
+ ALTER TABLE "Service" ADD COLUMN IF NOT EXISTS "verifiedAt" TIMESTAMP(3);
27
+ ALTER TABLE "Service" ADD COLUMN IF NOT EXISTS "licenseNumber" TEXT;
28
+ ALTER TABLE "Service" ADD COLUMN IF NOT EXISTS "certifications" TEXT[] DEFAULT ARRAY[]::TEXT[];
29
+ CREATE INDEX IF NOT EXISTS "Service_isVerified_idx" ON "Service"("isVerified");
30
+ CREATE INDEX IF NOT EXISTS "Service_isLicensed_idx" ON "Service"("isLicensed");
31
+
32
+ -- 4. User: Add organization relations (these are handled by Organization tables below)
33
+ -- Note: These are just relation fields in Prisma, no actual columns needed in User table
34
+
35
+ -- 5. Organization: Create complete organization system
36
+ -- ============================================================================
37
+
38
+ -- Create OrganizationType enum if it doesn't exist
39
+ DO $$ BEGIN
40
+ CREATE TYPE "OrganizationType" AS ENUM (
41
+ 'Nonprofit',
42
+ 'ForProfit',
43
+ 'Religious',
44
+ 'Educational',
45
+ 'Government',
46
+ 'Club',
47
+ 'Association',
48
+ 'Fraternity',
49
+ 'Sorority',
50
+ 'Union',
51
+ 'Cooperative',
52
+ 'Other'
53
+ );
54
+ EXCEPTION
55
+ WHEN duplicate_object THEN null;
56
+ END $$;
57
+
58
+ -- Create Organization table
59
+ CREATE TABLE IF NOT EXISTS "Organization" (
60
+ "id" TEXT NOT NULL PRIMARY KEY,
61
+ "name" TEXT NOT NULL,
62
+ "slug" TEXT NOT NULL UNIQUE,
63
+ "organizationType" "OrganizationType"[],
64
+ "bio" TEXT,
65
+ "mission" TEXT,
66
+ "pocName" TEXT,
67
+ "pocEmail" TEXT,
68
+ "pocPhone" TEXT,
69
+ "street" TEXT,
70
+ "city" TEXT,
71
+ "state" TEXT,
72
+ "zipCode" TEXT,
73
+ "country" TEXT DEFAULT 'US',
74
+ "coverPhoto" TEXT,
75
+ "logoUrl" TEXT,
76
+ "websiteUrl" TEXT,
77
+ "socialLinks" JSONB,
78
+ "goodsOrServices" TEXT[],
79
+ "venueTypes" TEXT[],
80
+ "secondaryVenueTypes" TEXT[] DEFAULT ARRAY[]::TEXT[],
81
+ "baseMembershipPriceCents" INTEGER,
82
+ "premiumMembershipPriceCents" INTEGER,
83
+ "allowGuestsByDefault" BOOLEAN DEFAULT false,
84
+ "defaultGuestLimit" INTEGER DEFAULT 0,
85
+ "visibility" TEXT DEFAULT 'Public',
86
+ "requiresApproval" BOOLEAN DEFAULT true,
87
+ "memberAutoApproval" BOOLEAN DEFAULT false,
88
+ "ownerId" TEXT NOT NULL,
89
+ "parentOrganizationId" TEXT,
90
+ "departmentName" TEXT,
91
+ "hierarchyLevel" INTEGER DEFAULT 0,
92
+ "subscriptionTier" TEXT,
93
+ "subscriptionStatus" TEXT,
94
+ "stripeSubscriptionId" TEXT UNIQUE,
95
+ "stripeCustomerId" TEXT,
96
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
97
+ "updatedAt" TIMESTAMP(3) NOT NULL,
98
+ CONSTRAINT "Organization_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
99
+ CONSTRAINT "Organization_parentOrganizationId_fkey" FOREIGN KEY ("parentOrganizationId") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE
100
+ );
101
+
102
+ CREATE INDEX IF NOT EXISTS "Organization_ownerId_idx" ON "Organization"("ownerId");
103
+ CREATE INDEX IF NOT EXISTS "Organization_slug_idx" ON "Organization"("slug");
104
+ CREATE INDEX IF NOT EXISTS "Organization_parentOrganizationId_idx" ON "Organization"("parentOrganizationId");
105
+
106
+ -- Create OrganizationMember table
107
+ CREATE TABLE IF NOT EXISTS "OrganizationMember" (
108
+ "id" TEXT NOT NULL PRIMARY KEY,
109
+ "organizationId" TEXT NOT NULL,
110
+ "userId" TEXT NOT NULL,
111
+ "role" TEXT DEFAULT 'Member',
112
+ "membershipTier" TEXT DEFAULT 'Base',
113
+ "status" TEXT DEFAULT 'Pending',
114
+ "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
115
+ "approvedAt" TIMESTAMP(3),
116
+ "approvedBy" TEXT,
117
+ "paidUntil" TIMESTAMP(3),
118
+ "paymentStatus" TEXT DEFAULT 'Current',
119
+ "guestLimit" INTEGER,
120
+ "canInvite" BOOLEAN DEFAULT true,
121
+ "canCreateEvents" BOOLEAN DEFAULT false,
122
+ "canManageBudgets" BOOLEAN DEFAULT false,
123
+ CONSTRAINT "OrganizationMember_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE,
124
+ CONSTRAINT "OrganizationMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
125
+ CONSTRAINT "OrganizationMember_organizationId_userId_key" UNIQUE ("organizationId", "userId")
126
+ );
127
+
128
+ CREATE INDEX IF NOT EXISTS "OrganizationMember_organizationId_idx" ON "OrganizationMember"("organizationId");
129
+ CREATE INDEX IF NOT EXISTS "OrganizationMember_userId_idx" ON "OrganizationMember"("userId");
130
+ CREATE INDEX IF NOT EXISTS "OrganizationMember_status_idx" ON "OrganizationMember"("status");
131
+
132
+ -- Create Department table
133
+ CREATE TABLE IF NOT EXISTS "Department" (
134
+ "id" TEXT NOT NULL PRIMARY KEY,
135
+ "organizationId" TEXT NOT NULL,
136
+ "name" TEXT NOT NULL,
137
+ "description" TEXT,
138
+ "parentDeptId" TEXT,
139
+ "headUserId" TEXT,
140
+ "budgetCents" INTEGER,
141
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
142
+ "updatedAt" TIMESTAMP(3) NOT NULL,
143
+ CONSTRAINT "Department_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE,
144
+ CONSTRAINT "Department_parentDeptId_fkey" FOREIGN KEY ("parentDeptId") REFERENCES "Department"("id") ON DELETE SET NULL ON UPDATE CASCADE,
145
+ CONSTRAINT "Department_headUserId_fkey" FOREIGN KEY ("headUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
146
+ );
147
+
148
+ CREATE INDEX IF NOT EXISTS "Department_organizationId_idx" ON "Department"("organizationId");
149
+ CREATE INDEX IF NOT EXISTS "Department_parentDeptId_idx" ON "Department"("parentDeptId");
150
+
151
+ -- Create OrganizationEvent table
152
+ CREATE TABLE IF NOT EXISTS "OrganizationEvent" (
153
+ "id" TEXT NOT NULL PRIMARY KEY,
154
+ "organizationId" TEXT NOT NULL,
155
+ "title" TEXT NOT NULL,
156
+ "description" TEXT,
157
+ "eventType" TEXT,
158
+ "startTime" TIMESTAMP(3) NOT NULL,
159
+ "endTime" TIMESTAMP(3),
160
+ "location" TEXT,
161
+ "maxAttendees" INTEGER,
162
+ "requiresRSVP" BOOLEAN DEFAULT true,
163
+ "allowGuests" BOOLEAN DEFAULT false,
164
+ "guestLimit" INTEGER DEFAULT 0,
165
+ "creatorId" TEXT NOT NULL,
166
+ "departmentId" TEXT,
167
+ "isPublic" BOOLEAN DEFAULT false,
168
+ "budgetCents" INTEGER,
169
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
170
+ "updatedAt" TIMESTAMP(3) NOT NULL,
171
+ CONSTRAINT "OrganizationEvent_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE,
172
+ CONSTRAINT "OrganizationEvent_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
173
+ CONSTRAINT "OrganizationEvent_departmentId_fkey" FOREIGN KEY ("departmentId") REFERENCES "Department"("id") ON DELETE SET NULL ON UPDATE CASCADE
174
+ );
175
+
176
+ CREATE INDEX IF NOT EXISTS "OrganizationEvent_organizationId_idx" ON "OrganizationEvent"("organizationId");
177
+ CREATE INDEX IF NOT EXISTS "OrganizationEvent_creatorId_idx" ON "OrganizationEvent"("creatorId");
178
+ CREATE INDEX IF NOT EXISTS "OrganizationEvent_startTime_idx" ON "OrganizationEvent"("startTime");
179
+
180
+ -- Create EventRSVP table
181
+ CREATE TABLE IF NOT EXISTS "EventRSVP" (
182
+ "id" TEXT NOT NULL PRIMARY KEY,
183
+ "eventId" TEXT NOT NULL,
184
+ "memberId" TEXT NOT NULL,
185
+ "userId" TEXT NOT NULL,
186
+ "status" TEXT DEFAULT 'Pending',
187
+ "guestCount" INTEGER DEFAULT 0,
188
+ "respondedAt" TIMESTAMP(3),
189
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
190
+ CONSTRAINT "EventRSVP_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "OrganizationEvent"("id") ON DELETE CASCADE ON UPDATE CASCADE,
191
+ CONSTRAINT "EventRSVP_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "OrganizationMember"("id") ON DELETE CASCADE ON UPDATE CASCADE,
192
+ CONSTRAINT "EventRSVP_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
193
+ CONSTRAINT "EventRSVP_eventId_memberId_key" UNIQUE ("eventId", "memberId")
194
+ );
195
+
196
+ CREATE INDEX IF NOT EXISTS "EventRSVP_eventId_idx" ON "EventRSVP"("eventId");
197
+ CREATE INDEX IF NOT EXISTS "EventRSVP_memberId_idx" ON "EventRSVP"("memberId");
198
+
199
+ -- Create OrganizationBudget table
200
+ CREATE TABLE IF NOT EXISTS "OrganizationBudget" (
201
+ "id" TEXT NOT NULL PRIMARY KEY,
202
+ "organizationId" TEXT NOT NULL,
203
+ "name" TEXT NOT NULL,
204
+ "description" TEXT,
205
+ "totalCents" INTEGER NOT NULL,
206
+ "spentCents" INTEGER DEFAULT 0,
207
+ "fiscalYear" INTEGER,
208
+ "startDate" TIMESTAMP(3) NOT NULL,
209
+ "endDate" TIMESTAMP(3) NOT NULL,
210
+ "approvedBy" TEXT,
211
+ "approvedAt" TIMESTAMP(3),
212
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
213
+ "updatedAt" TIMESTAMP(3) NOT NULL,
214
+ CONSTRAINT "OrganizationBudget_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE,
215
+ CONSTRAINT "OrganizationBudget_approvedBy_fkey" FOREIGN KEY ("approvedBy") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
216
+ );
217
+
218
+ CREATE INDEX IF NOT EXISTS "OrganizationBudget_organizationId_idx" ON "OrganizationBudget"("organizationId");
219
+
220
+ -- Create BudgetExpense table
221
+ CREATE TABLE IF NOT EXISTS "BudgetExpense" (
222
+ "id" TEXT NOT NULL PRIMARY KEY,
223
+ "budgetId" TEXT NOT NULL,
224
+ "eventId" TEXT,
225
+ "description" TEXT NOT NULL,
226
+ "amountCents" INTEGER NOT NULL,
227
+ "category" TEXT,
228
+ "submittedBy" TEXT NOT NULL,
229
+ "approvedBy" TEXT,
230
+ "approvedAt" TIMESTAMP(3),
231
+ "rejectedAt" TIMESTAMP(3),
232
+ "receiptUrl" TEXT,
233
+ "status" TEXT DEFAULT 'Pending',
234
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
235
+ CONSTRAINT "BudgetExpense_budgetId_fkey" FOREIGN KEY ("budgetId") REFERENCES "OrganizationBudget"("id") ON DELETE CASCADE ON UPDATE CASCADE,
236
+ "eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "OrganizationEvent"("id") ON DELETE SET NULL ON UPDATE CASCADE,
237
+ CONSTRAINT "BudgetExpense_submittedBy_fkey" FOREIGN KEY ("submittedBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE,
238
+ CONSTRAINT "BudgetExpense_approvedBy_fkey" FOREIGN KEY ("approvedBy") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
239
+ );
240
+
241
+ CREATE INDEX IF NOT EXISTS "BudgetExpense_budgetId_idx" ON "BudgetExpense"("budgetId");
242
+ CREATE INDEX IF NOT EXISTS "BudgetExpense_eventId_idx" ON "BudgetExpense"("eventId");
243
+
244
+ -- Create EventProposal table
245
+ CREATE TABLE IF NOT EXISTS "EventProposal" (
246
+ "id" TEXT NOT NULL PRIMARY KEY,
247
+ "organizationId" TEXT NOT NULL,
248
+ "title" TEXT NOT NULL,
249
+ "description" TEXT NOT NULL,
250
+ "proposedDate" TIMESTAMP(3),
251
+ "estimatedBudgetCents" INTEGER,
252
+ "creatorId" TEXT NOT NULL,
253
+ "status" TEXT DEFAULT 'Pending',
254
+ "votingDeadline" TIMESTAMP(3),
255
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
256
+ "updatedAt" TIMESTAMP(3) NOT NULL,
257
+ CONSTRAINT "EventProposal_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE,
258
+ CONSTRAINT "EventProposal_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE
259
+ );
260
+
261
+ CREATE INDEX IF NOT EXISTS "EventProposal_organizationId_idx" ON "EventProposal"("organizationId");
262
+ CREATE INDEX IF NOT EXISTS "EventProposal_status_idx" ON "EventProposal"("status");
263
+
264
+ -- Create ProposalVote table
265
+ CREATE TABLE IF NOT EXISTS "ProposalVote" (
266
+ "id" TEXT NOT NULL PRIMARY KEY,
267
+ "proposalId" TEXT NOT NULL,
268
+ "voterId" TEXT NOT NULL,
269
+ "vote" TEXT NOT NULL,
270
+ "comment" TEXT,
271
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
272
+ CONSTRAINT "ProposalVote_proposalId_fkey" FOREIGN KEY ("proposalId") REFERENCES "EventProposal"("id") ON DELETE CASCADE ON UPDATE CASCADE,
273
+ CONSTRAINT "ProposalVote_voterId_fkey" FOREIGN KEY ("voterId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
274
+ CONSTRAINT "ProposalVote_proposalId_voterId_key" UNIQUE ("proposalId", "voterId")
275
+ );
276
+
277
+ CREATE INDEX IF NOT EXISTS "ProposalVote_proposalId_idx" ON "ProposalVote"("proposalId");
278
+
279
+ -- Create _FavoriteOrganizations join table
280
+ CREATE TABLE IF NOT EXISTS "_FavoriteOrganizations" (
281
+ "A" TEXT NOT NULL,
282
+ "B" TEXT NOT NULL,
283
+ CONSTRAINT "_FavoriteOrganizations_A_fkey" FOREIGN KEY ("A") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE,
284
+ CONSTRAINT "_FavoriteOrganizations_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
285
+ );
286
+
287
+ CREATE UNIQUE INDEX IF NOT EXISTS "_FavoriteOrganizations_AB_unique" ON "_FavoriteOrganizations"("A", "B");
288
+ CREATE INDEX IF NOT EXISTS "_FavoriteOrganizations_B_index" ON "_FavoriteOrganizations"("B");
289
+
290
+ -- ============================================================================
291
+ -- VERIFICATION QUERIES
292
+ -- ============================================================================
293
+
294
+ -- Run these after migration to verify:
295
+ -- SELECT column_name FROM information_schema.columns WHERE table_name = 'BashFeedPost' AND column_name = 'serviceId';
296
+ -- SELECT column_name FROM information_schema.columns WHERE table_name = 'BashEvent' AND column_name = 'isAutoApprovable';
297
+ -- SELECT column_name FROM information_schema.columns WHERE table_name = 'Service' AND column_name = 'isVerified';
298
+ -- SELECT table_name FROM information_schema.tables WHERE table_name = 'Organization';
299
+
300
+ -- ============================================================================
301
+ -- END OF DELTA MIGRATION
302
+ -- ============================================================================
@@ -16,6 +16,7 @@ model Club {
16
16
  events BashEvent[]
17
17
  admin ClubAdmin[]
18
18
  members ClubMember[]
19
+ budgets Budget[] @relation("ClubBudgets")
19
20
  }
20
21
 
21
22
  model ClubAdmin {
@@ -56,7 +57,8 @@ model BashComment {
56
57
  model BashFeedPost {
57
58
  id String @id @default(cuid())
58
59
  userId String
59
- bashId String
60
+ bashId String?
61
+ serviceId String?
60
62
  content String? @db.Text
61
63
  mediaIds String[] // Array of Media IDs
62
64
  mentionedUserIds String[] // Array of User IDs mentioned in post
@@ -71,7 +73,8 @@ model BashFeedPost {
71
73
 
72
74
  // Relationships
73
75
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
74
- bash BashEvent @relation(fields: [bashId], references: [id], onDelete: Cascade)
76
+ bash BashEvent? @relation(fields: [bashId], references: [id], onDelete: Cascade)
77
+ service Service? @relation(fields: [serviceId], references: [id], onDelete: Cascade)
75
78
  likes BashFeedLike[]
76
79
  comments BashFeedComment[]
77
80
  ratings BashFeedRating[]
@@ -80,6 +83,7 @@ model BashFeedPost {
80
83
  reposts BashFeedRepost[]
81
84
 
82
85
  @@index([bashId])
86
+ @@index([serviceId])
83
87
  @@index([userId])
84
88
  @@index([createdAt])
85
89
  @@index([isAnonymous])
@@ -338,6 +342,7 @@ model EventTask {
338
342
  neededCount Int? // Number of volunteers needed
339
343
  estimatedDuration Int? // Estimated duration in minutes
340
344
  createdAt DateTime? @default(now())
345
+ organizationId String? // Organization association
341
346
  assignedTo User? @relation("TasksAssignedToMe", fields: [assignedToId], references: [id], onDelete: Cascade)
342
347
  bashEvent BashEvent @relation(fields: [bashEventId], references: [id], onDelete: Cascade)
343
348
  creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)
@@ -630,6 +635,16 @@ model BashEvent {
630
635
  autoApprovedAt DateTime?
631
636
  lastEngagementAt DateTime? // Track last interest/share for decay
632
637
 
638
+ // Organization field
639
+ organizationId String?
640
+ organization Organization? @relation("OrganizationBashEvents", fields: [organizationId], references: [id])
641
+
642
+ // Organization access controls (only apply when organizationId is set)
643
+ requiresOrgMembership Boolean @default(false) // Must be member to attend/RSVP
644
+ allowMemberGuests Boolean @default(false) // Members can bring +1s
645
+ maxGuestsPerMember Int? // Guest limit per member (null = unlimited)
646
+ restrictToOrgDepartments String[] @default([]) // Empty = all departments can see/join
647
+
633
648
  associatedBashesReferencingMe AssociatedBash[]
634
649
  comments BashComment[]
635
650
  amountOfGuests AmountOfGuests? @relation(fields: [amountOfGuestsId], references: [id], onDelete: Cascade)
@@ -689,9 +704,14 @@ model BashEvent {
689
704
 
690
705
  // Analytics Relations
691
706
  analyticsPredictions AnalyticsPrediction[]
707
+
708
+ // Organization Relations
709
+ budgets Budget[] @relation("EventBudgets")
710
+ rsvps EventRSVP[] @relation("EventRSVPs")
692
711
 
693
712
  @@index([templateId])
694
713
  @@index([parentEventId])
714
+ @@index([organizationId])
695
715
  }
696
716
 
697
717
  // ============================================
@@ -924,6 +944,12 @@ model TicketTier {
924
944
  waitList User[] @relation("TicketTierToUser")
925
945
  specialOffers SpecialOffer[] // NEW - Special offers for this tier
926
946
  appliedDiscounts AppliedDiscount[] // NEW - Discounts applied to this tier
947
+
948
+ // Organization member restrictions (for org events)
949
+ restrictedToRoles String[] @default([]) // ["Admin", "Owner"] - empty = all roles
950
+ restrictedToDepartments String[] @default([]) // [deptId1, deptId2] - empty = all departments
951
+ restrictedToMembershipTiers String[] @default([]) // ["Premium"] - empty = all membership tiers
952
+ isGuestTier Boolean @default(false) // Ticket for member +1s
927
953
 
928
954
  @@unique([bashEventId, title])
929
955
  }
@@ -1510,6 +1536,18 @@ model User {
1510
1536
 
1511
1537
  // Idea Interest Relations
1512
1538
  ideaInterests IdeaInterest[]
1539
+
1540
+ // Organization Relations
1541
+ ownedOrganizations Organization[] @relation("OrganizationOwner")
1542
+ organizationMemberships OrganizationMember[] @relation("OrganizationMemberUser")
1543
+ organizationEventRSVPs EventRSVP[] @relation("OrganizationEventRSVP")
1544
+ submittedExpenses Expense[] @relation("ExpenseSubmitter")
1545
+ approvedExpenses Expense[] @relation("ExpenseApprover")
1546
+ approvedBudgets Budget[] @relation("BudgetApprover")
1547
+ submittedBudgets Budget[] @relation("BudgetSubmitter")
1548
+ ownedBudgets Budget[] @relation("UserBudgets")
1549
+ departmentsHeaded Department[] @relation("DepartmentHead")
1550
+ favoriteOrganizations Organization[] @relation("FavoriteOrganizations")
1513
1551
  }
1514
1552
 
1515
1553
  model UserPreferences {
@@ -1952,6 +1990,12 @@ model Service {
1952
1990
  monthlyPrice Decimal @default(0)
1953
1991
  serviceListingStripeSubscriptionId String?
1954
1992
  acceptsBashCash Boolean @default(true) // NEW: Allow BashCash payments
1993
+ isVerified Boolean @default(false) // Service owner verified
1994
+ isLicensed Boolean @default(false) // Has required licenses
1995
+ verifiedAt DateTime? // When verification was approved
1996
+ licenseNumber String? // License/registration number
1997
+ certifications String[] @default([]) // Array of certification names
1998
+ bashFeedPosts BashFeedPost[]
1955
1999
  associatedServicesReferencingMe AssociatedService[]
1956
2000
  exhibitorBookingRequests ExhibitorBookingRequest[] @relation("ExhibitorBookingService")
1957
2001
  notification Notification[]
@@ -1987,6 +2031,8 @@ model Service {
1987
2031
 
1988
2032
  @@index([serviceListingStripeSubscriptionId])
1989
2033
  @@index([isFreeFirstListing])
2034
+ @@index([isVerified])
2035
+ @@index([isLicensed])
1990
2036
  }
1991
2037
 
1992
2038
  model StripeAccount {
@@ -2318,12 +2364,203 @@ model UserPromoCodeRedemption {
2318
2364
  }
2319
2365
 
2320
2366
  model Organization {
2321
- id String @id @default(cuid())
2322
- organizationType OrganizationType[]
2323
- goodsOrServices String[]
2324
- venueTypes String[]
2325
- secondaryVenueTypes String[] @default([])
2326
- service Service?
2367
+ id String @id @default(cuid())
2368
+ name String
2369
+ slug String @unique
2370
+ organizationType OrganizationType[]
2371
+ bio String?
2372
+ mission String?
2373
+ pocName String?
2374
+ pocEmail String?
2375
+ pocPhone String?
2376
+ street String?
2377
+ city String?
2378
+ state String?
2379
+ zipCode String?
2380
+ country String? @default("US")
2381
+ coverPhoto String?
2382
+ logoUrl String?
2383
+ websiteUrl String?
2384
+ socialLinks Json?
2385
+ goodsOrServices String[]
2386
+ venueTypes String[]
2387
+ secondaryVenueTypes String[] @default([])
2388
+ baseMembershipPriceCents Int?
2389
+ premiumMembershipPriceCents Int?
2390
+ allowGuestsByDefault Boolean @default(false)
2391
+ defaultGuestLimit Int @default(0)
2392
+ visibility String @default("Public")
2393
+ requiresApproval Boolean @default(true)
2394
+ memberAutoApproval Boolean @default(false)
2395
+ ownerId String
2396
+ parentOrganizationId String?
2397
+ departmentName String?
2398
+ hierarchyLevel Int @default(0)
2399
+ subscriptionTier String?
2400
+ subscriptionStatus String?
2401
+ stripeSubscriptionId String? @unique
2402
+ stripeCustomerId String?
2403
+ createdAt DateTime @default(now())
2404
+ updatedAt DateTime @updatedAt
2405
+ service Service?
2406
+ owner User @relation("OrganizationOwner", fields: [ownerId], references: [id], onDelete: Restrict)
2407
+ parentOrganization Organization? @relation("OrganizationHierarchy", fields: [parentOrganizationId], references: [id])
2408
+ childOrganizations Organization[] @relation("OrganizationHierarchy")
2409
+ members OrganizationMember[]
2410
+ budgets Budget[] @relation("OrganizationBudgets")
2411
+ departments Department[]
2412
+ bashEvents BashEvent[] @relation("OrganizationBashEvents")
2413
+ favoriteBy User[] @relation("FavoriteOrganizations")
2414
+
2415
+ @@index([ownerId])
2416
+ @@index([slug])
2417
+ @@index([parentOrganizationId])
2418
+ }
2419
+
2420
+ model OrganizationMember {
2421
+ id String @id @default(cuid())
2422
+ organizationId String
2423
+ userId String
2424
+ role String @default("Member") // Owner, Admin, Coordinator, EventOrganizer, Treasurer, Member
2425
+ membershipTier String @default("Base") // Base, Premium
2426
+ status String @default("Pending") // Pending, Active, Suspended, Removed
2427
+ joinedAt DateTime @default(now())
2428
+ approvedAt DateTime?
2429
+ approvedBy String?
2430
+ paidUntil DateTime?
2431
+ paymentStatus String? @default("Current") // Current, Overdue, Cancelled
2432
+ guestLimit Int?
2433
+ canInvite Boolean @default(true)
2434
+ canCreateEvents Boolean @default(false)
2435
+ canManageBudgets Boolean @default(false)
2436
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
2437
+ user User @relation("OrganizationMemberUser", fields: [userId], references: [id], onDelete: Cascade)
2438
+ eventRSVPs EventRSVP[]
2439
+
2440
+ @@unique([organizationId, userId])
2441
+ @@index([organizationId])
2442
+ @@index([userId])
2443
+ @@index([status])
2444
+ }
2445
+
2446
+ model Department {
2447
+ id String @id @default(cuid())
2448
+ organizationId String
2449
+ name String
2450
+ description String?
2451
+ parentDeptId String?
2452
+ headUserId String?
2453
+ budgetCents Int?
2454
+ createdAt DateTime @default(now())
2455
+ updatedAt DateTime @updatedAt
2456
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
2457
+ parentDept Department? @relation("DepartmentHierarchy", fields: [parentDeptId], references: [id])
2458
+ childDepts Department[] @relation("DepartmentHierarchy")
2459
+ head User? @relation("DepartmentHead", fields: [headUserId], references: [id])
2460
+ budgets Budget[] @relation("DepartmentBudgets")
2461
+
2462
+ @@index([organizationId])
2463
+ @@index([parentDeptId])
2464
+ }
2465
+
2466
+
2467
+ model EventRSVP {
2468
+ id String @id @default(cuid())
2469
+ eventId String
2470
+ memberId String
2471
+ userId String
2472
+ status String @default("Going") // Going, Maybe, NotGoing
2473
+ guestsCount Int @default(0)
2474
+ checkedIn Boolean @default(false)
2475
+ checkedInAt DateTime?
2476
+ createdAt DateTime @default(now())
2477
+ updatedAt DateTime @updatedAt
2478
+ event BashEvent @relation("EventRSVPs", fields: [eventId], references: [id], onDelete: Cascade)
2479
+ member OrganizationMember @relation(fields: [memberId], references: [id], onDelete: Cascade)
2480
+ user User @relation("OrganizationEventRSVP", fields: [userId], references: [id])
2481
+
2482
+ @@unique([eventId, userId])
2483
+ @@index([eventId])
2484
+ @@index([userId])
2485
+ }
2486
+
2487
+ model Budget {
2488
+ id String @id @default(cuid())
2489
+ name String
2490
+
2491
+ // Flexible ownership - at least ONE should be set
2492
+ organizationId String?
2493
+ bashEventId String?
2494
+ clubId String?
2495
+ departmentId String?
2496
+ userId String? // For personal event budgets
2497
+
2498
+ // Budget details
2499
+ totalAllocatedCents Int
2500
+ totalBudgetCents Int? // Alias for backward compatibility
2501
+ spentCents Int @default(0)
2502
+ remainingCents Int? // Can be computed: totalAllocatedCents - spentCents
2503
+
2504
+ // Categorization
2505
+ category String? // "Marketing", "Catering", "Venue", "Annual", "Activities"
2506
+ fiscalYear String? // Keep as String for flexibility ("2026", "2026-Q1", "FY2026")
2507
+
2508
+ // Approval workflow
2509
+ status String? @default("Draft") // Draft, Submitted, Approved, Active, Closed, Archived
2510
+ submittedBy String?
2511
+ approverId String?
2512
+ approvedBy String? // Alias for approverId
2513
+ approvedAt DateTime?
2514
+
2515
+ // Date ranges
2516
+ startDate DateTime?
2517
+ endDate DateTime?
2518
+
2519
+ // Timestamps
2520
+ createdAt DateTime @default(now())
2521
+ updatedAt DateTime @updatedAt
2522
+
2523
+ // Relations
2524
+ organization Organization? @relation("OrganizationBudgets", fields: [organizationId], references: [id], onDelete: Cascade)
2525
+ bashEvent BashEvent? @relation("EventBudgets", fields: [bashEventId], references: [id], onDelete: Cascade)
2526
+ club Club? @relation("ClubBudgets", fields: [clubId], references: [id], onDelete: Cascade)
2527
+ department Department? @relation("DepartmentBudgets", fields: [departmentId], references: [id], onDelete: Cascade)
2528
+ user User? @relation("UserBudgets", fields: [userId], references: [id], onDelete: Cascade)
2529
+ submitter User? @relation("BudgetSubmitter", fields: [submittedBy], references: [id])
2530
+ approver User? @relation("BudgetApprover", fields: [approverId], references: [id])
2531
+ expenses Expense[]
2532
+
2533
+ @@index([organizationId])
2534
+ @@index([bashEventId])
2535
+ @@index([clubId])
2536
+ @@index([departmentId])
2537
+ @@index([userId])
2538
+ @@index([status])
2539
+ @@index([fiscalYear])
2540
+ }
2541
+
2542
+ model Expense {
2543
+ id String @id @default(cuid())
2544
+ budgetId String
2545
+ submittedBy String
2546
+ description String
2547
+ amountCents Int
2548
+ category String?
2549
+ receiptUrl String?
2550
+ expenseDate DateTime? // When the expense occurred
2551
+ status String @default("Pending") // Pending, Approved, Rejected
2552
+ approvedBy String?
2553
+ approvedAt DateTime?
2554
+ rejectionReason String?
2555
+ createdAt DateTime @default(now())
2556
+ updatedAt DateTime @updatedAt
2557
+ budget Budget @relation(fields: [budgetId], references: [id], onDelete: Cascade)
2558
+ submitter User @relation("ExpenseSubmitter", fields: [submittedBy], references: [id])
2559
+ approver User? @relation("ExpenseApprover", fields: [approvedBy], references: [id])
2560
+
2561
+ @@index([budgetId])
2562
+ @@index([submittedBy])
2563
+ @@index([status])
2327
2564
  }
2328
2565
 
2329
2566
  model ServiceRange {
@@ -3729,11 +3966,6 @@ enum AudioVisualSupportSubType {
3729
3966
  ProjectorSetup
3730
3967
  }
3731
3968
 
3732
- enum HostingSupportSubType {
3733
- Emcee
3734
- Greeter
3735
- }
3736
-
3737
3969
  enum PromotionAndMarketingSubType {
3738
3970
  Influencer
3739
3971
  Promoter
@@ -3744,6 +3976,10 @@ enum StaffingSubType {
3744
3976
  CrewHand
3745
3977
  ParkingManagement
3746
3978
  GuestRegistration
3979
+ Greeter
3980
+ Ushers
3981
+ CoatCheck
3982
+ DoorAttendant
3747
3983
  Security
3748
3984
  }
3749
3985
 
@@ -4781,6 +5017,10 @@ enum OrganizationType {
4781
5017
  Youth
4782
5018
  Senior
4783
5019
  Tradeshow
5020
+ Professional
5021
+ RecordLabel
5022
+ School
5023
+ StudentClub
4784
5024
  Conference
4785
5025
  FoodAndBeverage
4786
5026
  TechAndStartups