@bantr-sports/sdk 12.10.0 → 12.11.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 (30) hide show
  1. package/api-type/src/modules/broadcast/broadcast.schema.d.ts +8 -0
  2. package/api-type/src/modules/broadcast/broadcast.service.d.ts +122 -1
  3. package/api-type/src/modules/broadcast/broadcast_content_hash.d.ts +17 -0
  4. package/api-type/src/modules/chat/chat.repository.d.ts +43 -1
  5. package/api-type/src/modules/chat/chat.schema.d.ts +2 -1
  6. package/api-type/src/modules/chat/chat.service.d.ts +23 -0
  7. package/api-type/src/modules/discount_code/discount_code.schema.d.ts +133 -2
  8. package/api-type/src/modules/marketplace/marketplace.schema.d.ts +1 -2
  9. package/api-type/src/modules/rules/rules.factory.d.ts +4 -0
  10. package/api-type/src/modules/rules/rules.schema.d.ts +67 -0
  11. package/api-type/src/modules/rules/rules.service.d.ts +38 -0
  12. package/api-type/src/modules/scout_feed/scout_feed.service.d.ts +6 -0
  13. package/api-type/src/modules/scout_feed/scout_feed_location.d.ts +27 -0
  14. package/api-type/src/modules/scout_profile/scout_soccer_profile.model.d.ts +2 -0
  15. package/api-type/src/modules/transaction/transaction.service.d.ts +1 -0
  16. package/api-type/src/trpcRouter/broadcast.router.d.ts +12 -0
  17. package/api-type/src/trpcRouter/chat.router.d.ts +32 -0
  18. package/api-type/src/trpcRouter/router.d.ts +452 -268
  19. package/api-type/src/trpcRouter/rules.router.d.ts +53 -0
  20. package/api-type/src/types/common.d.ts +1 -0
  21. package/api-type/src/types/game_stats.d.ts +0 -25
  22. package/api-type/src/types/league.d.ts +3 -1
  23. package/api-type/src/types/team.d.ts +2 -0
  24. package/dist/index.d.ts +732 -456
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/provider.d.ts +452 -268
  27. package/dist/provider.d.ts.map +1 -1
  28. package/dist/query.d.ts +194 -102
  29. package/dist/query.d.ts.map +1 -1
  30. package/package.json +1 -1
@@ -30,6 +30,14 @@ export declare const GetBroadcastByIdInputSchema: z.ZodObject<{
30
30
  id: number;
31
31
  }>;
32
32
  export type GetBroadcastByIdInput = z.infer<typeof GetBroadcastByIdInputSchema>;
33
+ export declare const GetBroadcastDeliveryStatusesInputSchema: z.ZodObject<{
34
+ broadcastId: z.ZodNumber;
35
+ }, "strip", z.ZodTypeAny, {
36
+ broadcastId: number;
37
+ }, {
38
+ broadcastId: number;
39
+ }>;
40
+ export type GetBroadcastDeliveryStatusesInput = z.infer<typeof GetBroadcastDeliveryStatusesInputSchema>;
33
41
  export declare const GetAudienceInputSchema: z.ZodObject<{
34
42
  seasonId: z.ZodOptional<z.ZodNumber>;
35
43
  leagueId: z.ZodOptional<z.ZodNumber>;
@@ -1,9 +1,30 @@
1
+ import type { Queue } from 'bullmq';
1
2
  import { Member } from '../../models/member';
2
3
  import { Broadcast } from '../../models/broadcast';
3
4
  import { BroadcastAudienceGroup } from '../../models/broadcast_audience_group';
4
5
  import { BroadcastAudienceMember } from '../../models/broadcast_audience_member';
5
- import { ArchiveBroadcastInput, DeleteBroadcastInput, DownloadBroadcastAudienceCsvInput, GetAudienceInput, GetBroadcastByIdInput, GetBroadcastsInput, SaveBroadcastInput, SendBroadcastInput } from './broadcast.schema';
6
+ import { ArchiveBroadcastInput, DeleteBroadcastInput, DownloadBroadcastAudienceCsvInput, GetAudienceInput, GetBroadcastByIdInput, GetBroadcastDeliveryStatusesInput, GetBroadcastsInput, SaveBroadcastInput, SendBroadcastInput } from './broadcast.schema';
6
7
  import { Organizer } from '../../models/organizer';
8
+ import type { BroadcastDeliveryStatus } from '../../models/broadcast_delivery';
9
+ /**
10
+ * `earlier_version` means a delivery exists, but of a message that has since been
11
+ * edited — so a resend correctly targets them again. `sending` distinguishes "no
12
+ * ledger row yet" on a broadcast still in flight from one that has finished.
13
+ */
14
+ export type BroadcastDeliverySummary = 'sent' | 'partial' | 'unknown' | 'failed' | 'earlier_version' | 'sending' | 'not_sent';
15
+ export type BroadcastMemberDeliveryStatus = {
16
+ memberId: number;
17
+ /** Outcome per channel for the content currently saved on the broadcast. */
18
+ email: BroadcastDeliveryStatus | null;
19
+ push: BroadcastDeliveryStatus | null;
20
+ notification: BroadcastDeliveryStatus | null;
21
+ summary: BroadcastDeliverySummary;
22
+ lastError: string | null;
23
+ deliveredAt: Date | null;
24
+ /** Highest attempt count across this member's channels — i.e. was this retried. */
25
+ attemptCount: number;
26
+ receivedEarlierVersion: boolean;
27
+ };
7
28
  export declare class BroadcastService {
8
29
  private readonly MemberModel;
9
30
  private readonly BroadcastModel;
@@ -14,10 +35,75 @@ export declare class BroadcastService {
14
35
  private assertOrganizationAccess;
15
36
  private getBroadcastForOrganization;
16
37
  private markBroadcastAsSending;
38
+ private normalizeFailureReason;
39
+ /**
40
+ * `note` is appended AFTER `normalizeFailureReason` truncates, so a long provider
41
+ * error cannot push it out of the message. The column is TEXT, so the combined
42
+ * length is not a concern. Used to warn that a resend may duplicate.
43
+ */
17
44
  private markBroadcastAsFailed;
45
+ /**
46
+ * Public safety net for the queue worker's `failed` handler. If a job dies or
47
+ * stalls before `processQueuedBroadcast` runs its own catch block (crash, OOM,
48
+ * lost lock), the broadcast would otherwise be stranded in SENDING forever.
49
+ * This resets it to FAILED so it can be resent. Idempotent: the underlying
50
+ * patch only matches rows still in SENDING, so a reason set by
51
+ * `processQueuedBroadcast`'s own catch is preserved.
52
+ */
53
+ failQueuedBroadcast(organizationId: number, broadcastId: number, reason?: string | null): Promise<void>;
18
54
  private assertOrganizationExists;
19
55
  private insertInChunks;
56
+ /**
57
+ * Retries a ledger write a few times before giving up.
58
+ *
59
+ * A dropped ledger row is not a cosmetic problem: `getDeliveredMemberIdsByChannel`
60
+ * treats "no row" as "not delivered", so a lost row means a resend mails that
61
+ * person again. A transient connection blip is worth a second attempt.
62
+ */
63
+ private withLedgerWriteRetry;
64
+ /**
65
+ * Members who already received `contentHash` on each channel.
66
+ *
67
+ * Scoped to `status = 'sent'`, so `unknown` (timed-out, possibly delivered) and
68
+ * `failed` recipients are re-targeted by construction: for game-day comms a
69
+ * missed notice is worse than a duplicate, and the ambiguous count is reported
70
+ * back to the organizer instead of being silently resolved either way.
71
+ */
72
+ private getDeliveredMemberIdsByChannel;
73
+ /**
74
+ * Records what was handed to a delivery channel, one autocommit statement per
75
+ * chunk, immediately after that batch is acknowledged.
76
+ *
77
+ * DELIBERATELY NOT IN A TRANSACTION (the `notification` channel excepted, which
78
+ * passes its `trx`). This is the entire point of the ledger: if checkpoints were
79
+ * written inside a transaction spanning the send, a crash mid-delivery would roll
80
+ * them all back, and the resend would duplicate everything — which is the bug
81
+ * this exists to fix. Order is send-then-record, so a crash in that gap re-sends
82
+ * one batch (at-least-once). Recording intent first would be at-most-once, i.e.
83
+ * silent drops.
84
+ */
85
+ private recordDeliveries;
86
+ /**
87
+ * Recipients whose delivery outcome is genuinely unknown — a provider request
88
+ * timed out without being aborted, so it may or may not have been delivered.
89
+ * Surfaced in `failure_reason` so the organizer knows a resend can duplicate.
90
+ */
91
+ private countAmbiguousDeliveries;
20
92
  private enqueueBroadcastSend;
93
+ /**
94
+ * Boot-time recovery for broadcasts stranded in SENDING. The worker's `failed`
95
+ * handler only fires for jobs that actually fail, so a row whose job never ran
96
+ * (Redis flushed, job ID collision, add rejected) or whose process died before
97
+ * that handler executed stays SENDING forever — it cannot be edited, resent, or
98
+ * completed. Rows younger than `staleAfterMs` are left alone so a legitimately
99
+ * slow send is never interrupted, and every patch is scoped to `status =
100
+ * 'SENDING'`, so concurrent API replicas running this cannot clobber a send that
101
+ * completed in the meantime.
102
+ */
103
+ reconcileStuckBroadcasts(queue: Pick<Queue, 'getJob'>, staleAfterMs?: number): Promise<{
104
+ checked: number;
105
+ reset: number;
106
+ }>;
21
107
  private assertWorkspaceAccess;
22
108
  private normalizeAudienceGroups;
23
109
  private applyBroadcastFilters;
@@ -27,6 +113,28 @@ export declare class BroadcastService {
27
113
  private stripHtmlForPushNotification;
28
114
  downloadBroadcastAudienceCsv(organizationId: number, { id }: DownloadBroadcastAudienceCsvInput): Promise<string | null>;
29
115
  getBroadcastById(organizationId: number, { id }: GetBroadcastByIdInput): Promise<Broadcast | null>;
116
+ /**
117
+ * Per-recipient delivery state for the Recipients table, read straight off the
118
+ * ledger. Answers the question a `FAILED` broadcast otherwise leaves open: who
119
+ * actually received this, and who would a resend re-target?
120
+ *
121
+ * Rows are grouped per member rather than per channel because that is how the
122
+ * table displays them; the per-channel statuses stay on the payload so the UI can
123
+ * show the breakdown without a second request.
124
+ */
125
+ getBroadcastDeliveryStatuses(organizationId: number, { broadcastId }: GetBroadcastDeliveryStatusesInput): Promise<{
126
+ contentHash: string;
127
+ members: BroadcastMemberDeliveryStatus[];
128
+ } | null>;
129
+ /**
130
+ * Collapses a member's per-channel statuses into the single label the table shows.
131
+ *
132
+ * Derived from the rows that exist, deliberately not from the broadcast's channel
133
+ * toggles: a member with push enabled but no registered device, or who muted
134
+ * broadcasts, is never handed to FCM and has no `push` row. Treating that as an
135
+ * incomplete delivery would label ordinary opt-outs as a problem.
136
+ */
137
+ private summarizeMemberDelivery;
30
138
  deleteBroadcast(organizationId: number, { id }: DeleteBroadcastInput): Promise<{
31
139
  success: boolean;
32
140
  id: number;
@@ -69,6 +177,7 @@ export declare class BroadcastService {
69
177
  pushNotification: boolean;
70
178
  senderEmail: string;
71
179
  status: string;
180
+ failureReason: string | null;
72
181
  isArchived: boolean;
73
182
  sentAt: Date | null;
74
183
  createdAt: Date;
@@ -91,8 +200,20 @@ export declare class BroadcastService {
91
200
  } | null>;
92
201
  processQueuedBroadcast(creatorId: number, organizationId: number, broadcastId: number): Promise<{
93
202
  broadcast: Broadcast;
203
+ /** Addresses handed to Mailgun — see `recordedEmailDeliveries` for delivered. */
94
204
  sentEmails: number;
205
+ /** Tokens handed to FCM — see `recordedPushDeliveries` for delivered. */
95
206
  sentPushNotifications: number;
96
207
  createdNotifications: number;
208
+ recordedEmailDeliveries: number;
209
+ ambiguousEmailDeliveries: number;
210
+ recordedPushDeliveries: number;
211
+ ambiguousPushDeliveries: number;
212
+ skippedAlreadyDelivered: number;
213
+ /**
214
+ * Reached, but missing from the ledger, so a resend cannot skip them.
215
+ * Non-zero means the ledger under-reports what was delivered.
216
+ */
217
+ unrecordedDeliveries: number;
97
218
  } | null>;
98
219
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Fingerprints the content a recipient actually received, so the delivery ledger
3
+ * can tell "already got this message" apart from "already got an earlier draft".
4
+ *
5
+ * A FAILED broadcast is fully editable: the organizer can reword the subject or
6
+ * body before hitting Resend. Skipping every previously-delivered recipient would
7
+ * mean the reworded version silently never reaches them. Keying the ledger on
8
+ * this hash instead makes an edited resend re-target the whole audience, while an
9
+ * unchanged resend targets only the recipients who were never reached.
10
+ *
11
+ * The tuple is JSON-encoded before hashing so that a subject/message split cannot
12
+ * be shifted without changing the digest — ('ab', 'c') and ('a', 'bc') hash
13
+ * differently. Channel toggles, sender, and audience are deliberately excluded:
14
+ * enabling push after an email-only send then finds no `push` rows and correctly
15
+ * targets everyone.
16
+ */
17
+ export declare const buildBroadcastContentHash: (subject?: string | null, message?: string | null) => string;
@@ -4,7 +4,7 @@ type PrismaClientLike = typeof prisma;
4
4
  type DbClient = any;
5
5
  type CreateContextChatInput = {
6
6
  actorId: number;
7
- chatType: typeof CHAT_TYPES.TEAM | typeof CHAT_TYPES.MATCH | typeof CHAT_TYPES.PICKUP;
7
+ chatType: typeof CHAT_TYPES.TEAM | typeof CHAT_TYPES.MATCH | typeof CHAT_TYPES.PICKUP_GROUP | typeof CHAT_TYPES.PICKUP;
8
8
  contextId: number;
9
9
  ownerId?: number | null;
10
10
  name?: string | null;
@@ -107,12 +107,54 @@ export declare class ChatRepository {
107
107
  getSharedTeamMemberIds(memberId: number, targetMemberIds: number[]): Promise<unknown[]>;
108
108
  getTeamContext(teamId: number): Promise<any>;
109
109
  getMatchContext(eventId: number): Promise<any>;
110
+ getNextTeamRsvpEvent(teamId: number, memberId: number): Promise<{
111
+ nextEvent: null;
112
+ hasRsvp: boolean;
113
+ rsvpStatus: string;
114
+ } | {
115
+ nextEvent: {
116
+ id: any;
117
+ title: any;
118
+ dateAndTime: any;
119
+ addressName: any;
120
+ location: any;
121
+ teamId: any;
122
+ timezone: any;
123
+ };
124
+ hasRsvp: boolean;
125
+ rsvpStatus: any;
126
+ }>;
127
+ getRsvpEvent(eventId: number, memberId: number): Promise<{
128
+ nextEvent: null;
129
+ hasRsvp: boolean;
130
+ rsvpStatus: string;
131
+ } | {
132
+ nextEvent: {
133
+ id: any;
134
+ title: any;
135
+ dateAndTime: any;
136
+ addressName: any;
137
+ location: any;
138
+ teamId: any;
139
+ timezone: any;
140
+ };
141
+ hasRsvp: boolean;
142
+ rsvpStatus: any;
143
+ }>;
110
144
  getPickupContext(pickupId: number): Promise<any>;
145
+ getPickupGroupContext(pickupGroupId: number): Promise<any>;
111
146
  getPickupMemberIds(pickupId: number): Promise<any>;
147
+ getPickupGroupMemberIds(pickupGroupId: number): Promise<any>;
112
148
  getActivePickupsForMember(memberId: number): Promise<any>;
149
+ getActivePickupGroupsForMember(memberId: number): Promise<any>;
150
+ private addContextMetadata;
113
151
  private chatMemberInclude;
114
152
  private messageInclude;
115
153
  private legacyChatType;
154
+ private emptyRsvpEventResponse;
155
+ private rsvpEventResponse;
156
+ private mapRsvpEvent;
157
+ private rsvpEventSelect;
116
158
  private sortMemberPair;
117
159
  private uniqueMemberIds;
118
160
  }
@@ -4,6 +4,7 @@ export declare const CHAT_TYPES: {
4
4
  readonly PRIVATE_GROUP: "private_group";
5
5
  readonly TEAM: "team";
6
6
  readonly MATCH: "match";
7
+ readonly PICKUP_GROUP: "pickup_group";
7
8
  readonly PICKUP: "pickup";
8
9
  };
9
10
  export declare const CHAT_MEMBER_ROLES: {
@@ -21,7 +22,7 @@ export declare const CHAT_MESSAGE_TYPES: {
21
22
  readonly POLL: "poll";
22
23
  readonly INVITE: "invite";
23
24
  };
24
- export declare const chatTypeSchema: z.ZodEnum<["direct", "private_group", "team", "match", "pickup"]>;
25
+ export declare const chatTypeSchema: z.ZodEnum<["direct", "private_group", "team", "match", "pickup_group", "pickup"]>;
25
26
  export declare const chatMemberRoleSchema: z.ZodEnum<["owner", "admin", "member"]>;
26
27
  export declare const listChatsInputSchema: z.ZodObject<{
27
28
  limit: z.ZodDefault<z.ZodNumber>;
@@ -27,6 +27,25 @@ export declare class ChatService {
27
27
  items: any;
28
28
  nextCursor: any;
29
29
  }>;
30
+ getNextRsvpEvent(memberId: number, input: {
31
+ chatId: number;
32
+ }): Promise<{
33
+ nextEvent: null;
34
+ hasRsvp: boolean;
35
+ rsvpStatus: string;
36
+ } | {
37
+ nextEvent: {
38
+ id: any;
39
+ title: any;
40
+ dateAndTime: any;
41
+ addressName: any;
42
+ location: any;
43
+ teamId: any;
44
+ timezone: any;
45
+ };
46
+ hasRsvp: boolean;
47
+ rsvpStatus: any;
48
+ }>;
30
49
  createDirectChat(actorId: number, input: CreateDirectChatInput): Promise<any>;
31
50
  createPrivateGroup(actorId: number, input: CreatePrivateGroupInput): Promise<any>;
32
51
  listGroupMemberCandidates(memberId: number, input: ListGroupMemberCandidatesInput): Promise<any[]>;
@@ -34,6 +53,8 @@ export declare class ChatService {
34
53
  syncTeamChat(teamId: number, actorId?: number): Promise<any>;
35
54
  getOrCreateMatchChat(actorId: number, input: ContextChatInput): Promise<any>;
36
55
  getOrCreatePickupChat(actorId: number, input: ContextChatInput): Promise<any>;
56
+ getOrCreatePickupGroupChat(actorId: number, input: ContextChatInput): Promise<any>;
57
+ syncPickupGroupChat(pickupGroupId: number, actorId?: number): Promise<any>;
37
58
  syncPickupChat(pickupId: number, actorId?: number): Promise<any>;
38
59
  sendMessage(actorId: number, input: SendMessageInput): Promise<any>;
39
60
  sendScoutInviteMessage(actorId: number, input: SendScoutInviteMessageInput): Promise<any>;
@@ -82,7 +103,9 @@ export declare class ChatService {
82
103
  private requireSharedTeamMembers;
83
104
  private requireMessageInChat;
84
105
  private ensureTeamChatsForMember;
106
+ private ensurePickupGroupChatsForMember;
85
107
  private ensurePickupChatsForMember;
86
108
  private uniqueMemberIds;
109
+ private formatPickupChatTitle;
87
110
  }
88
111
  export {};
@@ -118,7 +118,7 @@ export declare const GetDiscountCodeSchema: z.ZodObject<Pick<{
118
118
  id: number;
119
119
  }>;
120
120
  export type GetDiscountCodeSchema = z.infer<typeof GetDiscountCodeSchema>;
121
- export declare const CreateDiscountCodeSchema: z.ZodObject<Pick<{
121
+ export declare const CreateDiscountCodeFieldsSchema: z.ZodObject<Pick<{
122
122
  id: z.ZodNumber;
123
123
  code: z.ZodString;
124
124
  description: z.ZodString;
@@ -159,8 +159,115 @@ export declare const CreateDiscountCodeSchema: z.ZodObject<Pick<{
159
159
  validUntil: Date | null;
160
160
  orgWorkspaceId?: number | null | undefined;
161
161
  }>;
162
+ export declare const validateDiscountCodeAmount: (value: Pick<DiscountCodeSchema, "amountType" | "amountValue">, ctx: z.RefinementCtx) => void;
163
+ export declare const CreateDiscountCodeSchema: z.ZodEffects<z.ZodObject<Pick<{
164
+ id: z.ZodNumber;
165
+ code: z.ZodString;
166
+ description: z.ZodString;
167
+ appliesWhen: z.ZodEnum<["purchase"]>;
168
+ applicableOn: z.ZodNullable<z.ZodEnum<["league-registration", "pickup-registration", "free-agent-registration"]>>;
169
+ amountType: z.ZodEnum<["flat-rate", "percentage"]>;
170
+ amountValue: z.ZodNumber;
171
+ validFrom: z.ZodNullable<z.ZodDate>;
172
+ validUntil: z.ZodNullable<z.ZodEffects<z.ZodDate, Date, Date>>;
173
+ usageLimit: z.ZodNullable<z.ZodNumber>;
174
+ usageCount: z.ZodDefault<z.ZodNumber>;
175
+ stripePromotionCodeId: z.ZodNullable<z.ZodString>;
176
+ updatedAt: z.ZodDate;
177
+ createdAt: z.ZodDate;
178
+ deletedAt: z.ZodNullable<z.ZodDate>;
179
+ organizationId: z.ZodNumber;
180
+ orgWorkspaceId: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
181
+ leagueId: z.ZodNullable<z.ZodNumber>;
182
+ seasonId: z.ZodNullable<z.ZodNumber>;
183
+ }, "description" | "orgWorkspaceId" | "code" | "appliesWhen" | "applicableOn" | "amountType" | "amountValue" | "validFrom" | "validUntil">, "strip", z.ZodTypeAny, {
184
+ description: string;
185
+ code: string;
186
+ appliesWhen: "purchase";
187
+ applicableOn: "league-registration" | "pickup-registration" | "free-agent-registration" | null;
188
+ amountType: "flat-rate" | "percentage";
189
+ amountValue: number;
190
+ validFrom: Date | null;
191
+ validUntil: Date | null;
192
+ orgWorkspaceId?: number | null | undefined;
193
+ }, {
194
+ description: string;
195
+ code: string;
196
+ appliesWhen: "purchase";
197
+ applicableOn: "league-registration" | "pickup-registration" | "free-agent-registration" | null;
198
+ amountType: "flat-rate" | "percentage";
199
+ amountValue: number;
200
+ validFrom: Date | null;
201
+ validUntil: Date | null;
202
+ orgWorkspaceId?: number | null | undefined;
203
+ }>, {
204
+ description: string;
205
+ code: string;
206
+ appliesWhen: "purchase";
207
+ applicableOn: "league-registration" | "pickup-registration" | "free-agent-registration" | null;
208
+ amountType: "flat-rate" | "percentage";
209
+ amountValue: number;
210
+ validFrom: Date | null;
211
+ validUntil: Date | null;
212
+ orgWorkspaceId?: number | null | undefined;
213
+ }, {
214
+ description: string;
215
+ code: string;
216
+ appliesWhen: "purchase";
217
+ applicableOn: "league-registration" | "pickup-registration" | "free-agent-registration" | null;
218
+ amountType: "flat-rate" | "percentage";
219
+ amountValue: number;
220
+ validFrom: Date | null;
221
+ validUntil: Date | null;
222
+ orgWorkspaceId?: number | null | undefined;
223
+ }>;
162
224
  export type CreateDiscountCodeSchema = z.infer<typeof CreateDiscountCodeSchema>;
163
- export declare const UpdateDiscountCodeSchema: z.ZodObject<Pick<{
225
+ export declare const UpdateDiscountCodeFieldsSchema: z.ZodObject<Pick<{
226
+ id: z.ZodNumber;
227
+ code: z.ZodString;
228
+ description: z.ZodString;
229
+ appliesWhen: z.ZodEnum<["purchase"]>;
230
+ applicableOn: z.ZodNullable<z.ZodEnum<["league-registration", "pickup-registration", "free-agent-registration"]>>;
231
+ amountType: z.ZodEnum<["flat-rate", "percentage"]>;
232
+ amountValue: z.ZodNumber;
233
+ validFrom: z.ZodNullable<z.ZodDate>;
234
+ validUntil: z.ZodNullable<z.ZodEffects<z.ZodDate, Date, Date>>;
235
+ usageLimit: z.ZodNullable<z.ZodNumber>;
236
+ usageCount: z.ZodDefault<z.ZodNumber>;
237
+ stripePromotionCodeId: z.ZodNullable<z.ZodString>;
238
+ updatedAt: z.ZodDate;
239
+ createdAt: z.ZodDate;
240
+ deletedAt: z.ZodNullable<z.ZodDate>;
241
+ organizationId: z.ZodNumber;
242
+ orgWorkspaceId: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
243
+ leagueId: z.ZodNullable<z.ZodNumber>;
244
+ seasonId: z.ZodNullable<z.ZodNumber>;
245
+ }, "id" | "description" | "orgWorkspaceId" | "code" | "appliesWhen" | "applicableOn" | "amountType" | "amountValue" | "validFrom" | "validUntil" | "usageLimit">, "strip", z.ZodTypeAny, {
246
+ id: number;
247
+ description: string;
248
+ code: string;
249
+ appliesWhen: "purchase";
250
+ applicableOn: "league-registration" | "pickup-registration" | "free-agent-registration" | null;
251
+ amountType: "flat-rate" | "percentage";
252
+ amountValue: number;
253
+ validFrom: Date | null;
254
+ validUntil: Date | null;
255
+ usageLimit: number | null;
256
+ orgWorkspaceId?: number | null | undefined;
257
+ }, {
258
+ id: number;
259
+ description: string;
260
+ code: string;
261
+ appliesWhen: "purchase";
262
+ applicableOn: "league-registration" | "pickup-registration" | "free-agent-registration" | null;
263
+ amountType: "flat-rate" | "percentage";
264
+ amountValue: number;
265
+ validFrom: Date | null;
266
+ validUntil: Date | null;
267
+ usageLimit: number | null;
268
+ orgWorkspaceId?: number | null | undefined;
269
+ }>;
270
+ export declare const UpdateDiscountCodeSchema: z.ZodEffects<z.ZodObject<Pick<{
164
271
  id: z.ZodNumber;
165
272
  code: z.ZodString;
166
273
  description: z.ZodString;
@@ -204,6 +311,30 @@ export declare const UpdateDiscountCodeSchema: z.ZodObject<Pick<{
204
311
  validUntil: Date | null;
205
312
  usageLimit: number | null;
206
313
  orgWorkspaceId?: number | null | undefined;
314
+ }>, {
315
+ id: number;
316
+ description: string;
317
+ code: string;
318
+ appliesWhen: "purchase";
319
+ applicableOn: "league-registration" | "pickup-registration" | "free-agent-registration" | null;
320
+ amountType: "flat-rate" | "percentage";
321
+ amountValue: number;
322
+ validFrom: Date | null;
323
+ validUntil: Date | null;
324
+ usageLimit: number | null;
325
+ orgWorkspaceId?: number | null | undefined;
326
+ }, {
327
+ id: number;
328
+ description: string;
329
+ code: string;
330
+ appliesWhen: "purchase";
331
+ applicableOn: "league-registration" | "pickup-registration" | "free-agent-registration" | null;
332
+ amountType: "flat-rate" | "percentage";
333
+ amountValue: number;
334
+ validFrom: Date | null;
335
+ validUntil: Date | null;
336
+ usageLimit: number | null;
337
+ orgWorkspaceId?: number | null | undefined;
207
338
  }>;
208
339
  export type UpdateDiscountCodeSchema = z.infer<typeof UpdateDiscountCodeSchema>;
209
340
  export declare const DeleteDiscountCodeSchema: z.ZodObject<Pick<{
@@ -20,8 +20,7 @@ export declare enum Sports {
20
20
  Soccer = "soccer",
21
21
  Softball = "softball",
22
22
  Volleyball = "volleyball",
23
- Basketball = "basketball",
24
- FlagBall = "flag_ball"
23
+ Basketball = "basketball"
25
24
  }
26
25
  export declare enum Gender {
27
26
  Male = "male",
@@ -0,0 +1,4 @@
1
+ import { RulesService } from './rules.service';
2
+ export declare class RulesFactory {
3
+ static createInstance(): RulesService;
4
+ }
@@ -0,0 +1,67 @@
1
+ import { z } from 'zod';
2
+ export declare const GetWorkspaceRulesInputSchema: z.ZodObject<{
3
+ organizationId: z.ZodNumber;
4
+ workspaceId: z.ZodNumber;
5
+ }, "strip", z.ZodTypeAny, {
6
+ organizationId: number;
7
+ workspaceId: number;
8
+ }, {
9
+ organizationId: number;
10
+ workspaceId: number;
11
+ }>;
12
+ export declare const PublishWorkspaceRulesInputSchema: z.ZodObject<{
13
+ organizationId: z.ZodNumber;
14
+ workspaceId: z.ZodNumber;
15
+ content: z.ZodString;
16
+ }, "strip", z.ZodTypeAny, {
17
+ content: string;
18
+ organizationId: number;
19
+ workspaceId: number;
20
+ }, {
21
+ content: string;
22
+ organizationId: number;
23
+ workspaceId: number;
24
+ }>;
25
+ export declare const DeleteWorkspaceRulesInputSchema: z.ZodObject<{
26
+ organizationId: z.ZodNumber;
27
+ workspaceId: z.ZodNumber;
28
+ }, "strip", z.ZodTypeAny, {
29
+ organizationId: number;
30
+ workspaceId: number;
31
+ }, {
32
+ organizationId: number;
33
+ workspaceId: number;
34
+ }>;
35
+ /** Same shape as the organizer read - the marketplace just reaches it without a session. */
36
+ export declare const GetPublicWorkspaceRulesInputSchema: z.ZodObject<{
37
+ organizationId: z.ZodNumber;
38
+ workspaceId: z.ZodNumber;
39
+ }, "strip", z.ZodTypeAny, {
40
+ organizationId: number;
41
+ workspaceId: number;
42
+ }, {
43
+ organizationId: number;
44
+ workspaceId: number;
45
+ }>;
46
+ export type GetWorkspaceRulesInput = z.infer<typeof GetWorkspaceRulesInputSchema>;
47
+ export type PublishWorkspaceRulesInput = z.infer<typeof PublishWorkspaceRulesInputSchema>;
48
+ export type DeleteWorkspaceRulesInput = z.infer<typeof DeleteWorkspaceRulesInputSchema>;
49
+ export type GetPublicWorkspaceRulesInput = z.infer<typeof GetPublicWorkspaceRulesInputSchema>;
50
+ export type WorkspaceRules = {
51
+ organizationId: number;
52
+ workspaceId: number;
53
+ content: string;
54
+ version: number;
55
+ publishedAt: Date;
56
+ updatedAt: Date;
57
+ publishedBy: {
58
+ id: number;
59
+ firstName: string;
60
+ lastName: string;
61
+ } | null;
62
+ };
63
+ /**
64
+ * The player-facing read. Deliberately drops `publishedBy` - the marketplace product page
65
+ * is reachable without a session, and which organizer pressed publish is not public info.
66
+ */
67
+ export type PublicWorkspaceRules = Omit<WorkspaceRules, 'publishedBy'>;
@@ -0,0 +1,38 @@
1
+ import { type DeleteWorkspaceRulesInput, type GetPublicWorkspaceRulesInput, type GetWorkspaceRulesInput, type PublicWorkspaceRules, type PublishWorkspaceRulesInput, type WorkspaceRules } from './rules.schema';
2
+ export declare class RulesService {
3
+ /**
4
+ * Reads carry `organizationId` from the caller, so it has to be checked against the
5
+ * workspace - otherwise passing someone else's `workspaceId` alongside your own
6
+ * `organizationId` would read their rules.
7
+ */
8
+ private assertWorkspaceInOrganization;
9
+ private assertOrganizerInOrganization;
10
+ /** Only `publishedBy` needs mapping - the rest of the row is already camelCase. */
11
+ private toWorkspaceRules;
12
+ /**
13
+ * Returns `null` rather than throwing when a workspace has no rules yet - that is the
14
+ * empty state the settings page renders, not an error.
15
+ *
16
+ * This read exposes `publishedBy`, which the public read deliberately withholds, so the
17
+ * caller has to be an organizer of the organization and not merely signed in.
18
+ */
19
+ getWorkspaceRules(userId: number, input: GetWorkspaceRulesInput): Promise<WorkspaceRules | null>;
20
+ /**
21
+ * Unauthenticated read for the marketplace product page. Published rules are already
22
+ * public by intent, so the only guard is the same workspace/organization pairing check;
23
+ * `publishedBy` is not selected so no organizer identity leaks to anonymous visitors.
24
+ *
25
+ * `null` means "this workspace has not published rules", which the modal renders as its
26
+ * empty state rather than as a failure.
27
+ */
28
+ getPublicWorkspaceRules(input: GetPublicWorkspaceRulesInput): Promise<PublicWorkspaceRules | null>;
29
+ /**
30
+ * Create-or-replace: the workspace keeps a single live document and every publish
31
+ * bumps `version`, so "add rules" and "edit rules" are the same write.
32
+ */
33
+ publishWorkspaceRules(userId: number, input: PublishWorkspaceRulesInput): Promise<WorkspaceRules>;
34
+ /** Takes the workspace back to the empty state. */
35
+ deleteWorkspaceRules(userId: number, input: DeleteWorkspaceRulesInput): Promise<{
36
+ deleted: boolean;
37
+ }>;
38
+ }
@@ -57,6 +57,8 @@ export declare class ScoutFeedService {
57
57
  distanceFromEventInMiles?: number;
58
58
  age?: number;
59
59
  currentCity?: string;
60
+ longitude?: number | null;
61
+ latitude?: number | null;
60
62
  QueryBuilderType: import("objection-js-soft-delete").SoftDeleteQueryBuilder<ScoutSoccerProfile, ScoutSoccerProfile[]> & import("objection").QueryBuilder<ScoutSoccerProfile, ScoutSoccerProfile[]>;
61
63
  $modelClass: import("objection").ModelClass<ScoutSoccerProfile>;
62
64
  })[];
@@ -96,6 +98,8 @@ export declare class ScoutFeedService {
96
98
  distanceFromEventInMiles?: number;
97
99
  age?: number;
98
100
  currentCity?: string;
101
+ longitude?: number | null;
102
+ latitude?: number | null;
99
103
  QueryBuilderType: import("objection-js-soft-delete").SoftDeleteQueryBuilder<ScoutSoccerProfile, ScoutSoccerProfile[]> & import("objection").QueryBuilder<ScoutSoccerProfile, ScoutSoccerProfile[]>;
100
104
  $modelClass: import("objection").ModelClass<ScoutSoccerProfile>;
101
105
  })[];
@@ -143,6 +147,8 @@ export declare class ScoutFeedService {
143
147
  distanceFromEventInMiles?: number;
144
148
  age?: number;
145
149
  currentCity?: string;
150
+ longitude?: number | null;
151
+ latitude?: number | null;
146
152
  QueryBuilderType: import("objection-js-soft-delete").SoftDeleteQueryBuilder<ScoutSoccerProfile, ScoutSoccerProfile[]> & import("objection").QueryBuilder<ScoutSoccerProfile, ScoutSoccerProfile[]>;
147
153
  $modelClass: import("objection").ModelClass<ScoutSoccerProfile>;
148
154
  }[];
@@ -0,0 +1,27 @@
1
+ type TCoordinatesRow = {
2
+ longitude?: number | null;
3
+ latitude?: number | null;
4
+ } | undefined;
5
+ /**
6
+ * BAN-1924: the member's own scout profile location (set when they created/edited their
7
+ * scout profile) is a more reliable "where do I want to be scouted" signal than their
8
+ * general account address, so it takes priority when no venue has been explicitly chosen.
9
+ */
10
+ export declare function resolveScoutFeedDefaultCoordinates(scoutProfileCoordinates: TCoordinatesRow, memberCoordinates: TCoordinatesRow): {
11
+ lng?: number;
12
+ lat?: number;
13
+ };
14
+ /**
15
+ * Throws when the requesting member has no usable location anywhere (no explicit
16
+ * venue, no scout profile location, no account address). PRECONDITION_FAILED lets
17
+ * clients distinguish this expected "no location set" state from a malformed request
18
+ * so they can degrade gracefully (eg. prompt for a location) instead of erroring out.
19
+ */
20
+ export declare function assertHasScoutFeedLocation(coordinates?: {
21
+ lat?: number | null;
22
+ lng?: number | null;
23
+ } | null): asserts coordinates is {
24
+ lat: number;
25
+ lng: number;
26
+ };
27
+ export {};
@@ -35,6 +35,8 @@ export declare class ScoutSoccerProfile extends ScoutSoccerProfile_base {
35
35
  distanceFromEventInMiles?: number;
36
36
  age?: number;
37
37
  currentCity?: string;
38
+ longitude?: number | null;
39
+ latitude?: number | null;
38
40
  static get tableName(): string;
39
41
  static get idColumn(): string;
40
42
  static get relationMappings(): RelationMappings;