@battle-racing/br-common-lib 4.3.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/rules/01-naming-and-typing.md +14 -0
- package/.agents/rules/02-architecture-and-routing.md +15 -0
- package/.agents/rules/03-state-and-data.md +10 -0
- package/.agents/rules/04-ui-and-styling.md +10 -0
- package/.agents/rules/05-code-quality-and-practices.md +10 -0
- package/dist/__deprecated__/types/ticketing/GameTurn.schema.d.ts +5 -5
- package/dist/__deprecated__/types/ticketing/GameTurnTask.schema.d.ts +2 -2
- package/dist/domains/player/Player.schema.d.ts +2 -8
- package/dist/domains/player/Player.schema.js +3 -7
- package/dist/domains/racingTeam/RacingTeam.schema.d.ts +77 -3
- package/dist/domains/racingTeam/RacingTeam.schema.js +18 -4
- package/dist/domains/racingTeam/RacingTeam.types.d.ts +4 -1
- package/dist/domains/session/Session.const.d.ts +2 -1
- package/dist/domains/session/Session.const.js +2 -1
- package/dist/domains/session/Session.schema.d.ts +144 -8
- package/dist/domains/session/Session.schema.js +19 -3
- package/dist/domains/session/Session.types.d.ts +4 -1
- package/dist/domains/ticketing/Ticketing.schema.d.ts +6 -0
- package/dist/domains/ticketing/Ticketing.schema.js +5 -0
- package/dist/domains/user/User.schema.d.ts +56 -7
- package/dist/domains/user/User.schema.js +44 -12
- package/dist/domains/user/User.types.d.ts +6 -1
- package/docs/TICKETING_AND_PURCHASE_FLOW.md +110 -0
- package/package.json +1 -1
- package/src/domains/player/Player.schema.ts +2 -7
- package/src/domains/racingTeam/RacingTeam.schema.ts +20 -3
- package/src/domains/racingTeam/RacingTeam.types.ts +7 -0
- package/src/domains/session/Session.const.ts +2 -1
- package/src/domains/session/Session.schema.ts +22 -2
- package/src/domains/session/Session.types.ts +6 -0
- package/src/domains/ticketing/Ticketing.schema.ts +5 -0
- package/src/domains/user/User.schema.ts +52 -14
- package/src/domains/user/User.types.ts +15 -1
|
@@ -59,6 +59,8 @@ export declare const purchaseSchema: z.ZodObject<{
|
|
|
59
59
|
id: z.ZodUUID;
|
|
60
60
|
purchaseNumber: z.ZodNumber;
|
|
61
61
|
buyerUserId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
62
|
+
guestName: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
63
|
+
guestContact: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
62
64
|
items: z.ZodArray<z.ZodObject<{
|
|
63
65
|
ticketType: z.ZodEnum<{
|
|
64
66
|
readonly Race: "Race";
|
|
@@ -85,6 +87,8 @@ export declare const purchaseSchema: z.ZodObject<{
|
|
|
85
87
|
}, z.core.$strip>;
|
|
86
88
|
export declare const createPurchaseInputSchema: z.ZodObject<{
|
|
87
89
|
buyerUserId: z.ZodOptional<z.ZodString>;
|
|
90
|
+
guestName: z.ZodOptional<z.ZodString>;
|
|
91
|
+
guestContact: z.ZodOptional<z.ZodString>;
|
|
88
92
|
items: z.ZodArray<z.ZodObject<{
|
|
89
93
|
ticketType: z.ZodEnum<{
|
|
90
94
|
readonly Race: "Race";
|
|
@@ -133,6 +137,7 @@ export declare const gameTurnTaskSchema: z.ZodObject<{
|
|
|
133
137
|
export declare const gameTurnSchema: z.ZodObject<{
|
|
134
138
|
id: z.ZodString;
|
|
135
139
|
purchaseId: z.ZodString;
|
|
140
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
136
141
|
leaderPlayerId: z.ZodString;
|
|
137
142
|
gameTickets: z.ZodArray<z.ZodString>;
|
|
138
143
|
playerIds: z.ZodArray<z.ZodString>;
|
|
@@ -166,6 +171,7 @@ export declare const gameTurnSchema: z.ZodObject<{
|
|
|
166
171
|
export declare const gameTurnQueueItemSchema: z.ZodObject<{
|
|
167
172
|
id: z.ZodString;
|
|
168
173
|
purchaseId: z.ZodString;
|
|
174
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
169
175
|
leaderPlayerId: z.ZodString;
|
|
170
176
|
gameTickets: z.ZodArray<z.ZodString>;
|
|
171
177
|
playerIds: z.ZodArray<z.ZodString>;
|
|
@@ -31,6 +31,8 @@ exports.purchaseSchema = zod_1.z.object({
|
|
|
31
31
|
purchaseNumber: zod_1.z.number(),
|
|
32
32
|
// Buyer can be null for Guest/On-Site purchases
|
|
33
33
|
buyerUserId: zod_1.z.string().optional().nullable(),
|
|
34
|
+
guestName: zod_1.z.string().optional().nullable(),
|
|
35
|
+
guestContact: zod_1.z.string().optional().nullable(),
|
|
34
36
|
items: zod_1.z.array(exports.purchaseItemSchema),
|
|
35
37
|
totalAmountCents: zod_1.z.number().min(0),
|
|
36
38
|
status: exports.purchaseStatusSchema.default(Ticketing_const_1.PURCHASE_STATUS.PENDING),
|
|
@@ -43,6 +45,8 @@ exports.purchaseSchema = zod_1.z.object({
|
|
|
43
45
|
});
|
|
44
46
|
exports.createPurchaseInputSchema = zod_1.z.object({
|
|
45
47
|
buyerUserId: zod_1.z.string().optional(),
|
|
48
|
+
guestName: zod_1.z.string().optional(),
|
|
49
|
+
guestContact: zod_1.z.string().optional(),
|
|
46
50
|
items: zod_1.z.array(exports.purchaseItemSchema).min(1),
|
|
47
51
|
});
|
|
48
52
|
exports.gameTurnStatusSchema = zod_1.z.enum(Ticketing_const_1.GAME_TURN_STATUS);
|
|
@@ -60,6 +64,7 @@ exports.gameTurnTaskSchema = zod_1.z.object({
|
|
|
60
64
|
exports.gameTurnSchema = zod_1.z.object({
|
|
61
65
|
id: zod_1.z.string(),
|
|
62
66
|
purchaseId: zod_1.z.string(),
|
|
67
|
+
sessionId: zod_1.z.string().optional(),
|
|
63
68
|
leaderPlayerId: zod_1.z.string(),
|
|
64
69
|
gameTickets: zod_1.z.array(zod_1.z.string()),
|
|
65
70
|
playerIds: zod_1.z.array(zod_1.z.string()),
|
|
@@ -4,10 +4,13 @@ export declare const userTypeSchema: z.ZodEnum<{
|
|
|
4
4
|
readonly PLAYER: "PLAYER";
|
|
5
5
|
}>;
|
|
6
6
|
export declare const PHONE_REGEX: RegExp;
|
|
7
|
+
export declare const emailOrPhoneSchema: z.ZodString;
|
|
7
8
|
export declare const userSchema: z.ZodObject<{
|
|
8
9
|
id: z.ZodUUID;
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
name: z.ZodString;
|
|
11
|
+
lastName: z.ZodString;
|
|
12
|
+
email: z.ZodNullable<z.ZodOptional<z.ZodEmail>>;
|
|
13
|
+
phoneNumber: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
11
14
|
role: z.ZodEnum<{
|
|
12
15
|
readonly SUPER_ADMIN: "SUPER_ADMIN";
|
|
13
16
|
readonly OPERATOR: "OPERATOR";
|
|
@@ -25,10 +28,39 @@ export declare const userSchema: z.ZodObject<{
|
|
|
25
28
|
createdAt: z.ZodDate;
|
|
26
29
|
lastLoginAt: z.ZodOptional<z.ZodDate>;
|
|
27
30
|
}, z.core.$strip>;
|
|
31
|
+
/**
|
|
32
|
+
* The schema for the user returned on login
|
|
33
|
+
*/
|
|
34
|
+
export declare const authUserSchema: z.ZodObject<{
|
|
35
|
+
id: z.ZodUUID;
|
|
36
|
+
name: z.ZodString;
|
|
37
|
+
lastName: z.ZodString;
|
|
38
|
+
email: z.ZodNullable<z.ZodOptional<z.ZodEmail>>;
|
|
39
|
+
phoneNumber: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
40
|
+
role: z.ZodEnum<{
|
|
41
|
+
readonly SUPER_ADMIN: "SUPER_ADMIN";
|
|
42
|
+
readonly OPERATOR: "OPERATOR";
|
|
43
|
+
readonly PLAYER: "PLAYER";
|
|
44
|
+
readonly CASHIER: "CASHIER";
|
|
45
|
+
}>;
|
|
46
|
+
type: z.ZodEnum<{
|
|
47
|
+
readonly STAFF: "STAFF";
|
|
48
|
+
readonly PLAYER: "PLAYER";
|
|
49
|
+
}>;
|
|
50
|
+
isActive: z.ZodDefault<z.ZodBoolean>;
|
|
51
|
+
isEmailVerified: z.ZodDefault<z.ZodBoolean>;
|
|
52
|
+
isPhoneVerified: z.ZodDefault<z.ZodBoolean>;
|
|
53
|
+
defaultPlayerId: z.ZodOptional<z.ZodUUID>;
|
|
54
|
+
createdAt: z.ZodDate;
|
|
55
|
+
lastLoginAt: z.ZodOptional<z.ZodDate>;
|
|
56
|
+
authToken: z.ZodString;
|
|
57
|
+
}, z.core.$strip>;
|
|
28
58
|
export declare const userWithPasswordSchema: z.ZodIntersection<z.ZodObject<{
|
|
29
59
|
id: z.ZodUUID;
|
|
30
|
-
|
|
31
|
-
|
|
60
|
+
name: z.ZodString;
|
|
61
|
+
lastName: z.ZodString;
|
|
62
|
+
email: z.ZodNullable<z.ZodOptional<z.ZodEmail>>;
|
|
63
|
+
phoneNumber: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
32
64
|
role: z.ZodEnum<{
|
|
33
65
|
readonly SUPER_ADMIN: "SUPER_ADMIN";
|
|
34
66
|
readonly OPERATOR: "OPERATOR";
|
|
@@ -48,10 +80,27 @@ export declare const userWithPasswordSchema: z.ZodIntersection<z.ZodObject<{
|
|
|
48
80
|
}, z.core.$strip>, z.ZodObject<{
|
|
49
81
|
password: z.ZodString;
|
|
50
82
|
}, z.core.$strip>>;
|
|
83
|
+
export declare const userInitialRegistrationSchema: z.ZodObject<{
|
|
84
|
+
emailOrPhone: z.ZodString;
|
|
85
|
+
password: z.ZodString;
|
|
86
|
+
}, z.core.$strip>;
|
|
87
|
+
export declare const userVerifySchema: z.ZodObject<{
|
|
88
|
+
emailOrPhone: z.ZodString;
|
|
89
|
+
code: z.ZodString;
|
|
90
|
+
}, z.core.$strip>;
|
|
91
|
+
export declare const userCompleteProfileSchema: z.ZodObject<{
|
|
92
|
+
name: z.ZodString;
|
|
93
|
+
lastName: z.ZodOptional<z.ZodString>;
|
|
94
|
+
nickname: z.ZodString;
|
|
95
|
+
birthDate: z.ZodCoercedDate<unknown>;
|
|
96
|
+
waiverSignature: z.ZodBoolean;
|
|
97
|
+
teamName: z.ZodOptional<z.ZodString>;
|
|
98
|
+
}, z.core.$strip>;
|
|
51
99
|
export declare const userRegistrationInputSchema: z.ZodObject<{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
100
|
+
name: z.ZodString;
|
|
101
|
+
email: z.ZodNullable<z.ZodOptional<z.ZodEmail>>;
|
|
102
|
+
lastName: z.ZodString;
|
|
103
|
+
phoneNumber: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
55
104
|
password: z.ZodString;
|
|
56
105
|
nickname: z.ZodString;
|
|
57
106
|
birthDate: z.ZodCoercedDate<unknown>;
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.userLoginInputSchema = exports.userRegistrationInputSchema = exports.userWithPasswordSchema = exports.userSchema = exports.PHONE_REGEX = exports.userTypeSchema = void 0;
|
|
3
|
+
exports.userLoginInputSchema = exports.userRegistrationInputSchema = exports.userCompleteProfileSchema = exports.userVerifySchema = exports.userInitialRegistrationSchema = exports.userWithPasswordSchema = exports.authUserSchema = exports.userSchema = exports.emailOrPhoneSchema = exports.PHONE_REGEX = exports.userTypeSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const role_1 = require("../role");
|
|
6
6
|
const User_const_1 = require("./User.const");
|
|
7
7
|
exports.userTypeSchema = zod_1.z.enum(User_const_1.USER_TYPE);
|
|
8
8
|
exports.PHONE_REGEX = /^([+]?[\\s0-9]+)?(\\d{3}|[(]?[0-9]+[)])?([-]?[\\s]?[0-9])+$/;
|
|
9
9
|
const MIN_PASSWORD_LENGTH = 6;
|
|
10
|
+
exports.emailOrPhoneSchema = zod_1.z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1, 'Email or phone is required')
|
|
13
|
+
.refine((val) => {
|
|
14
|
+
const isEmail = zod_1.z.email().safeParse(val).success;
|
|
15
|
+
const isPhone = exports.PHONE_REGEX.test(val);
|
|
16
|
+
return isEmail || isPhone;
|
|
17
|
+
}, { message: 'Must be a valid email or phone number' });
|
|
10
18
|
const baseUserObj = zod_1.z.object({
|
|
11
19
|
id: zod_1.z.uuidv4(),
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
name: zod_1.z.string(),
|
|
21
|
+
lastName: zod_1.z.string(),
|
|
22
|
+
email: zod_1.z.email().toLowerCase().optional().nullable(),
|
|
23
|
+
phoneNumber: zod_1.z.string().regex(exports.PHONE_REGEX, 'Invalid phone number format').optional().nullable(),
|
|
14
24
|
// --- ACCESS CONTROL ---
|
|
15
25
|
role: role_1.userRoleSchema,
|
|
16
26
|
type: exports.userTypeSchema,
|
|
@@ -26,16 +36,45 @@ exports.userSchema = baseUserObj.refine((data) => data.email || data.phoneNumber
|
|
|
26
36
|
message: 'User must have either an email or a phone number',
|
|
27
37
|
path: ['email', 'phoneNumber'],
|
|
28
38
|
});
|
|
39
|
+
/**
|
|
40
|
+
* The schema for the user returned on login
|
|
41
|
+
*/
|
|
42
|
+
exports.authUserSchema = baseUserObj.extend({
|
|
43
|
+
authToken: zod_1.z.string(),
|
|
44
|
+
});
|
|
29
45
|
exports.userWithPasswordSchema = exports.userSchema.and(zod_1.z.object({
|
|
30
46
|
password: zod_1.z.string().min(MIN_PASSWORD_LENGTH),
|
|
31
47
|
}));
|
|
48
|
+
exports.userInitialRegistrationSchema = zod_1.z.object({
|
|
49
|
+
emailOrPhone: exports.emailOrPhoneSchema,
|
|
50
|
+
password: zod_1.z.string().min(MIN_PASSWORD_LENGTH, `Password must be at least ${MIN_PASSWORD_LENGTH} characters`),
|
|
51
|
+
});
|
|
52
|
+
exports.userVerifySchema = zod_1.z.object({
|
|
53
|
+
emailOrPhone: exports.emailOrPhoneSchema,
|
|
54
|
+
code: zod_1.z.string().min(6, 'Code must be at least 6 characters').max(10, 'Code is too long'),
|
|
55
|
+
});
|
|
56
|
+
exports.userCompleteProfileSchema = baseUserObj
|
|
57
|
+
.pick({
|
|
58
|
+
name: true,
|
|
59
|
+
})
|
|
60
|
+
.extend({
|
|
61
|
+
lastName: zod_1.z.string().optional(),
|
|
62
|
+
nickname: zod_1.z.string().min(2),
|
|
63
|
+
birthDate: zod_1.z.coerce.date(),
|
|
64
|
+
waiverSignature: zod_1.z.boolean().refine((val) => val === true, {
|
|
65
|
+
message: 'You must accept the waiver signature',
|
|
66
|
+
}),
|
|
67
|
+
teamName: zod_1.z.string().optional(),
|
|
68
|
+
});
|
|
69
|
+
// Deprecated in favor of multi-step registration
|
|
32
70
|
exports.userRegistrationInputSchema = baseUserObj
|
|
33
71
|
.pick({
|
|
72
|
+
name: true,
|
|
73
|
+
lastName: true,
|
|
34
74
|
email: true,
|
|
35
75
|
phoneNumber: true,
|
|
36
76
|
})
|
|
37
77
|
.extend({
|
|
38
|
-
name: zod_1.z.string().optional(),
|
|
39
78
|
password: zod_1.z.string().min(6),
|
|
40
79
|
nickname: zod_1.z.string().min(2),
|
|
41
80
|
birthDate: zod_1.z.coerce.date(),
|
|
@@ -45,13 +84,6 @@ exports.userRegistrationInputSchema = baseUserObj
|
|
|
45
84
|
path: ['email', 'phoneNumber'],
|
|
46
85
|
});
|
|
47
86
|
exports.userLoginInputSchema = zod_1.z.object({
|
|
48
|
-
emailOrPhone:
|
|
49
|
-
.string()
|
|
50
|
-
.min(1, 'Email or phone is required')
|
|
51
|
-
.refine((val) => {
|
|
52
|
-
const isEmail = zod_1.z.email().safeParse(val).success;
|
|
53
|
-
const isPhone = exports.PHONE_REGEX.test(val);
|
|
54
|
-
return isEmail || isPhone;
|
|
55
|
-
}, { message: 'Must be a valid email or phone number' }),
|
|
87
|
+
emailOrPhone: exports.emailOrPhoneSchema,
|
|
56
88
|
password: zod_1.z.string().min(MIN_PASSWORD_LENGTH, `Password must be at least ${MIN_PASSWORD_LENGTH} characters`),
|
|
57
89
|
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { userSchema, userTypeSchema, userWithPasswordSchema } from './User.schema';
|
|
2
|
+
import { userSchema, userTypeSchema, userWithPasswordSchema, userInitialRegistrationSchema, userVerifySchema, userCompleteProfileSchema, userLoginInputSchema, authUserSchema } from './User.schema';
|
|
3
3
|
export type UserType = z.infer<typeof userTypeSchema>;
|
|
4
4
|
export type User = z.infer<typeof userSchema>;
|
|
5
5
|
export type UserWithPassword = z.infer<typeof userWithPasswordSchema>;
|
|
6
|
+
export type AuthUser = z.infer<typeof authUserSchema>;
|
|
7
|
+
export type UserInitialRegistration = z.infer<typeof userInitialRegistrationSchema>;
|
|
8
|
+
export type UserVerify = z.infer<typeof userVerifySchema>;
|
|
9
|
+
export type UserCompleteProfile = z.infer<typeof userCompleteProfileSchema>;
|
|
10
|
+
export type UserLogin = z.infer<typeof userLoginInputSchema>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Flujo de Compra de Tickets y Separación User/Player
|
|
2
|
+
|
|
3
|
+
Este documento explica de forma detallada cómo está estructurado el flujo de compra de tickets en **Battle Racing**, cómo se separa la entidad que paga (User) de la entidad que corre (Player), qué hemos implementado hasta ahora y qué nos hace falta por implementar.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 👥 1. Separación: Users vs. Players
|
|
8
|
+
|
|
9
|
+
Para asegurar "cero fricción" en el entorno físico de la pista, hemos separado estrictamente el concepto de la cuenta (quien paga) del corredor (quien se sube al Kart).
|
|
10
|
+
|
|
11
|
+
### `User` (La Cuenta / El Pagador)
|
|
12
|
+
|
|
13
|
+
- **Propósito:** La persona física que se registra, gestiona un método de pago y posee el historial transaccional de compras.
|
|
14
|
+
- **Autenticación:** Usa `email` o `phoneNumber` (sin `username`), contraseña y roles (`SUPER_ADMIN`, `CASHIER`, `PLAYER`).
|
|
15
|
+
- **Relaciones:**
|
|
16
|
+
- Tiene un `defaultPlayerId` que apunta a su propio perfil de corredor.
|
|
17
|
+
- Puede ser tutor y ser "dueño" (`ownerUserId`) de múltiples `Players` menores de edad (`MINOR`).
|
|
18
|
+
|
|
19
|
+
### `Player` (El Corredor / Perfil Gamificado)
|
|
20
|
+
|
|
21
|
+
- **Propósito:** Es la entidad gamificada que se asigna físicamente a un Kart y cuyo nombre sale en las pantallas.
|
|
22
|
+
- **Identificador (`nickname`):** No son únicos en la base de datos. Varios corredores pueden llamarse "Maverick" al mismo tiempo para no generar errores al registrarse rápidamente en el POS.
|
|
23
|
+
- **Waivers:** Contiene el estado de la firma del documento de exención de responsabilidad (`waiver`).
|
|
24
|
+
- **Relaciones:** Si es un invitado anónimo (`isGuest`), no está atado a ningún `User`. Si es un menor, su firma (`waiver`) es implícitamente otorgada por el `User` tutor.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🎟️ 2. Flujo de Compras (Purchases y Tickets)
|
|
29
|
+
|
|
30
|
+
La adquisición de carreras sigue un ciclo de vida de tres grandes capas:
|
|
31
|
+
|
|
32
|
+
### Capa 1: La Compra (`Purchase`)
|
|
33
|
+
|
|
34
|
+
1. **Creación:** Se crea un `Purchase` agrupando varios ítems de compra (ej. 3 tickets de tipo `RACE`).
|
|
35
|
+
- Se crea en el endpoint: `POST /ticketing/purchases`
|
|
36
|
+
- Si el cliente está logueado en la Web App, se asocia su `buyerUserId`.
|
|
37
|
+
- Si la compra es 100% anónima en la sucursal (POS), `buyerUserId` es `null` y se usan `guestName` y `guestContact`.
|
|
38
|
+
2. **Pago:** El pago se confirma asíncronamente vía Webhooks (`POST /ticketing/webhook/payment`) o manualmente en caja registradora (`POST /ticketing/purchases/:id/manual-payment`).
|
|
39
|
+
3. **Generación de GameTickets:** Una vez confirmada y pagada la compra, esta se _desfragmenta_ y la base de datos genera los `GameTicket` individuales correspondientes (con estado `CREATED`).
|
|
40
|
+
|
|
41
|
+
### Capa 2: La Sesión (`Game Session`)
|
|
42
|
+
|
|
43
|
+
Dado que una persona puede comprar 10 tickets, pero a la pista se van a subir sus 5 amigos 2 veces, se usa una **Session**.
|
|
44
|
+
|
|
45
|
+
1. **Creación:** Se agrupan tickets recién comprados bajo un código alfanumérico corto (ej. `A1B2C3`).
|
|
46
|
+
2. **Unión:** Los amigos escanean un QR o dictan el código para unirse a la sesión (`POST /sessions/:code/join`). Aquí sus `Players` ingresan a la sesión.
|
|
47
|
+
3. **Asignación:** Se asigna cada `GameTicket` disponible en la sesión a un `playerId` específico (`POST /sessions/:code/tickets/assign`).
|
|
48
|
+
|
|
49
|
+
### Capa 3: El Turno Físico en Pista (`Game Turn`)
|
|
50
|
+
|
|
51
|
+
1. **Cola de Carrera:** Cuando el grupo de la sesión está listo (o el operador del POS organiza la carrera), se crea un `GameTurn`.
|
|
52
|
+
- Endpoint: `POST /game-turns` o `POST /game-turns/bulk`.
|
|
53
|
+
2. **Vinculación Final:** El `GameTurn` toma los `GameTickets` asignados y los `playerIds` correspondientes, bloqueándolos para esta carrera específica.
|
|
54
|
+
3. **Ejecución y Hardware:** El hardware vincula a los jugadores con sus respectivos Karts y avanza las tareas de la carrera (preparación, en sus marcas, carrera, meta).
|
|
55
|
+
- El progreso se guarda mediante: `POST /game-turns/:id/tasks/:taskId/complete`
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## ✅ 3. ¿Qué tenemos implementado hasta ahora?
|
|
60
|
+
|
|
61
|
+
### En `br-common-lib` (Schemas)
|
|
62
|
+
|
|
63
|
+
- Zod schemas altamente detallados para `User`, `Player`, `Session`, `Ticketing` (Purchase y Tickets) y `Payment`.
|
|
64
|
+
- Control absoluto y fuente de verdad centralizada para la validación de estos modelos.
|
|
65
|
+
|
|
66
|
+
### En `game-manager-backend` (REST API)
|
|
67
|
+
|
|
68
|
+
- **Módulo `ticketing`:**
|
|
69
|
+
- Creación de compras.
|
|
70
|
+
- Procesamiento manual de pagos (para POS/Cashiers).
|
|
71
|
+
- Cancelación de compras.
|
|
72
|
+
- Recepción de webhooks de pasarelas de pago.
|
|
73
|
+
- Listado mis compras y resumen de mis tickets (`my-purchases`, `my-tickets`).
|
|
74
|
+
- **Módulo `session`:**
|
|
75
|
+
- Obtener sesión por código corto.
|
|
76
|
+
- Unirse (y unirse en bulk) a una sesión.
|
|
77
|
+
- Asignar tickets a jugadores dentro de la sesión.
|
|
78
|
+
- **Módulo `game-turn`:**
|
|
79
|
+
- Listado general de turnos (`GET /game-turns`).
|
|
80
|
+
- Creación de turnos y turnos en bloque.
|
|
81
|
+
- Avance de etapas (tasks) dentro del turno (`completeTask`).
|
|
82
|
+
- Capacidad para avanzar (`/advance`) o posponer (`/postpone`) el turno en la fila.
|
|
83
|
+
- **Módulo `player` & `user`:**
|
|
84
|
+
- Estructuras en DB/Repositorios y controladores listos para manipular estos perfiles.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 🚧 4. ¿Qué nos hace falta implementar? (Faltantes / TODOs)
|
|
89
|
+
|
|
90
|
+
1. **Autenticación Severa (Guards):**
|
|
91
|
+
- Varios enpoints importantes como la creación de compras y modificación de sesiones todavía usan `request.user?.id` asumiendo que el usuario está inyectado, pero faltan **Guards** (e.g., JWT Guards) sólidos y globales para denegar acceso a usuarios no autorizados de forma segura o están marcados como `@Public()`.
|
|
92
|
+
|
|
93
|
+
2. **Validación de Identidad de Email/Teléfono (`User`):**
|
|
94
|
+
- Aunque la base de datos soporta `isEmailVerified` e `isPhoneVerified`, el mecanismo de envío de OTP / SMS / Enlace Mágico para confirmar la propiedad aún no está programado.
|
|
95
|
+
|
|
96
|
+
3. **Verificación de Seguridad de Asignación de Tickets:**
|
|
97
|
+
- Comprobar que un User no pueda asignar tickets de un Purchase que NO le pertenece (cuando la abstracción de sesión permita saltos de seguridad).
|
|
98
|
+
- Actualmente falta un "Ownership Check" robusto en varios GET de compras y lectura de turnos.
|
|
99
|
+
|
|
100
|
+
4. **Actualización Automática de Estadísticas del Player:**
|
|
101
|
+
- Actualmente la base de datos de Players tiene sub-schemas listos para Stats (`totalRaces`, `bestTimeMs`, `xp`). Sin embargo, una vez que el `GameTurn` se completa en la pista, falta implementar el webhook o servicio que **calcule los resultados finales y actualice estos stats** dentro de cada `Player` que participó.
|
|
102
|
+
|
|
103
|
+
5. **Flujos de Error de Pagos y Reembolsos Avanzados:**
|
|
104
|
+
- Manejo exhaustivo de reembolsos desde Stripe hacia el sistema para revocar `GameTickets` si una persona pide chargeback manual.
|
|
105
|
+
|
|
106
|
+
6. **Integración Completa con POS (Front-end UI):**
|
|
107
|
+
- Falta validar en el frontend de POS que estos flujos asíncronos (como el crear compra en cash, asignar players de forma anónima) no se bloqueen por dependencias de cuentas "Guest". Asegurarse de que el frontend POS pueda transitar estas 3 capas sin recargar.
|
|
108
|
+
|
|
109
|
+
7. **Asociación Dinámica de Hardware (Karts ↔ Players) en el GameTurn:**
|
|
110
|
+
- Hay que madurar la lógica donde un `playerId` que entra a un `GameTurn` es asignado al IP / Socket / ID Hardware específico del Kart en el que se acaba de sentar.
|
package/package.json
CHANGED
|
@@ -37,7 +37,8 @@ export const playerSchema = z.object({
|
|
|
37
37
|
export const createPlayerSchema = playerSchema
|
|
38
38
|
.pick({
|
|
39
39
|
nickname: true,
|
|
40
|
-
|
|
40
|
+
ownerUserId: true,
|
|
41
|
+
isGuest: true,
|
|
41
42
|
})
|
|
42
43
|
.extend({
|
|
43
44
|
birthDate: z.coerce.date(),
|
|
@@ -47,9 +48,3 @@ export const signWaiverSchema = z.object({
|
|
|
47
48
|
signature: z.string().optional(), // Base64 or simply a boolean acceptance. Let's make it flexible.
|
|
48
49
|
accepted: z.literal(true), // They must explicitly pass accepted: true
|
|
49
50
|
});
|
|
50
|
-
|
|
51
|
-
export const createMinorPlayerSchema = z.object({
|
|
52
|
-
nickname: z.string().min(2).max(20),
|
|
53
|
-
birthDate: z.coerce.date(),
|
|
54
|
-
// For a minor, the signature is implicitly granted by the tutor calling this endpoint
|
|
55
|
-
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { paginatedResponseSchema } from '../pagination';
|
|
3
3
|
import { JOIN_REQUEST_STATUS, RACING_TEAM_ROLE } from './RacingTeam.const';
|
|
4
|
+
import { playerSchema } from '../player/Player.schema';
|
|
4
5
|
|
|
5
6
|
// --- Team Schemas ---
|
|
6
7
|
|
|
@@ -44,25 +45,41 @@ export const paginatedRacingTeamSummarySchema = paginatedResponseSchema(racingTe
|
|
|
44
45
|
export const joinRequestSchema = z.object({
|
|
45
46
|
id: z.string(),
|
|
46
47
|
racingTeamId: z.string(),
|
|
47
|
-
|
|
48
|
+
playerId: z.string(),
|
|
48
49
|
status: z.enum(JOIN_REQUEST_STATUS),
|
|
49
50
|
createdAt: z.date(),
|
|
50
51
|
updatedAt: z.date(),
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
export const teamMemberSchema = z.object({
|
|
55
|
+
id: z.string(),
|
|
54
56
|
racingTeamId: z.string(),
|
|
55
|
-
|
|
57
|
+
playerId: z.string(),
|
|
58
|
+
role: z.enum(RACING_TEAM_ROLE),
|
|
59
|
+
joinedAt: z.date(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const teamMemberWithPlayerSchema = playerSchema.extend({
|
|
56
63
|
role: z.enum(RACING_TEAM_ROLE),
|
|
57
64
|
joinedAt: z.date(),
|
|
58
65
|
});
|
|
59
66
|
|
|
67
|
+
export const racingTeamWithMembersSchema = racingTeamSchema.extend({
|
|
68
|
+
members: z.array(teamMemberWithPlayerSchema),
|
|
69
|
+
});
|
|
70
|
+
|
|
60
71
|
export const createJoinRequestSchema = z.object({
|
|
61
72
|
racingTeamId: z.string(),
|
|
73
|
+
playerId: z.string(),
|
|
62
74
|
});
|
|
63
75
|
|
|
64
76
|
export const answerJoinRequestSchema = z.object({
|
|
65
77
|
racingTeamId: z.string(),
|
|
66
|
-
|
|
78
|
+
targetPlayerId: z.string(),
|
|
67
79
|
accepted: z.boolean(),
|
|
68
80
|
});
|
|
81
|
+
|
|
82
|
+
export const addTeamMemberSchema = z.object({
|
|
83
|
+
nickname: z.string().min(2).max(20),
|
|
84
|
+
birthDate: z.coerce.date(),
|
|
85
|
+
});
|
|
@@ -6,10 +6,13 @@ import {
|
|
|
6
6
|
joinRequestSchema,
|
|
7
7
|
paginatedRacingTeamSchema,
|
|
8
8
|
paginatedRacingTeamSummarySchema,
|
|
9
|
+
racingTeamWithMembersSchema,
|
|
9
10
|
racingTeamSchema,
|
|
10
11
|
racingTeamSummarySchema,
|
|
12
|
+
teamMemberWithPlayerSchema,
|
|
11
13
|
teamMemberSchema,
|
|
12
14
|
updateRacingTeamSchema,
|
|
15
|
+
addTeamMemberSchema,
|
|
13
16
|
} from './RacingTeam.schema';
|
|
14
17
|
|
|
15
18
|
export type RacingTeam = z.infer<typeof racingTeamSchema>;
|
|
@@ -23,3 +26,7 @@ export type JoinRequest = z.infer<typeof joinRequestSchema>;
|
|
|
23
26
|
export type TeamMember = z.infer<typeof teamMemberSchema>;
|
|
24
27
|
export type CreateJoinRequest = z.infer<typeof createJoinRequestSchema>;
|
|
25
28
|
export type AnswerJoinRequest = z.infer<typeof answerJoinRequestSchema>;
|
|
29
|
+
|
|
30
|
+
export type TeamMemberWithPlayer = z.infer<typeof teamMemberWithPlayerSchema>;
|
|
31
|
+
export type RacingTeamWithMembers = z.infer<typeof racingTeamWithMembersSchema>;
|
|
32
|
+
export type AddTeamMember = z.infer<typeof addTeamMemberSchema>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { SESSION_STATUS } from './Session.const';
|
|
3
3
|
import { gameTypeSchema } from '../game/Game.schema';
|
|
4
|
+
import { playerSchema } from '../player/Player.schema';
|
|
5
|
+
import { gameTicketSchema, gameTurnSchema } from '../ticketing/Ticketing.schema';
|
|
4
6
|
|
|
5
7
|
export const sessionStatusSchema = z.enum(SESSION_STATUS);
|
|
6
8
|
|
|
@@ -10,20 +12,26 @@ export const sessionTicketSchema = z.object({
|
|
|
10
12
|
});
|
|
11
13
|
|
|
12
14
|
export const gameSessionSchema = z.object({
|
|
15
|
+
id: z.uuidv4(),
|
|
13
16
|
code: z.string().length(6), // 6-char alphanumeric
|
|
14
17
|
contactPhone: z.string().optional(),
|
|
15
18
|
userId: z.string().optional().nullable(),
|
|
16
19
|
|
|
17
|
-
tickets: z.array(
|
|
20
|
+
tickets: z.array(gameTicketSchema).optional(),
|
|
18
21
|
playerIds: z.array(z.uuidv4()),
|
|
19
22
|
linkedPurchaseId: z.string().optional(),
|
|
20
23
|
|
|
21
|
-
status: sessionStatusSchema.default(SESSION_STATUS.
|
|
24
|
+
status: sessionStatusSchema.default(SESSION_STATUS.REGISTRATION),
|
|
22
25
|
|
|
23
26
|
createdAt: z.date(),
|
|
24
27
|
updatedAt: z.date(),
|
|
25
28
|
});
|
|
26
29
|
|
|
30
|
+
export const gameSessionWithPlayersSchema = gameSessionSchema.extend({
|
|
31
|
+
players: z.array(playerSchema),
|
|
32
|
+
turns: z.array(gameTurnSchema).optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
27
35
|
export const createSessionInputSchema = z.object({
|
|
28
36
|
contactPhone: z.string().optional(),
|
|
29
37
|
userId: z.string().optional().nullable(),
|
|
@@ -35,6 +43,14 @@ export const joinSessionSchema = z.object({
|
|
|
35
43
|
birthDate: z.coerce.date(),
|
|
36
44
|
});
|
|
37
45
|
|
|
46
|
+
export const addExistingPlayersSchema = z.object({
|
|
47
|
+
playerIds: z.array(z.uuidv4()).min(1),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const updateSessionStatusSchema = z.object({
|
|
51
|
+
status: sessionStatusSchema,
|
|
52
|
+
});
|
|
53
|
+
|
|
38
54
|
export const createGameTurnInputSchema = z.object({
|
|
39
55
|
assignments: z
|
|
40
56
|
.array(
|
|
@@ -56,3 +72,7 @@ export const assignSessionTicketsInputSchema = z.object({
|
|
|
56
72
|
)
|
|
57
73
|
.min(1),
|
|
58
74
|
});
|
|
75
|
+
|
|
76
|
+
export const bulkCreateGameTurnSchema = z.object({
|
|
77
|
+
turns: z.array(createGameTurnInputSchema).min(1),
|
|
78
|
+
});
|
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
assignSessionTicketsInputSchema,
|
|
4
4
|
createSessionInputSchema,
|
|
5
5
|
gameSessionSchema,
|
|
6
|
+
gameSessionWithPlayersSchema,
|
|
7
|
+
joinSessionSchema,
|
|
8
|
+
bulkCreateGameTurnSchema,
|
|
6
9
|
sessionStatusSchema,
|
|
7
10
|
sessionTicketSchema,
|
|
8
11
|
} from './Session.schema';
|
|
@@ -10,5 +13,8 @@ import {
|
|
|
10
13
|
export type SessionStatus = z.infer<typeof sessionStatusSchema>;
|
|
11
14
|
export type SessionTicket = z.infer<typeof sessionTicketSchema>;
|
|
12
15
|
export type GameSession = z.infer<typeof gameSessionSchema>;
|
|
16
|
+
export type GameSessionWithPlayers = z.infer<typeof gameSessionWithPlayersSchema>;
|
|
13
17
|
export type CreateSessionInput = z.infer<typeof createSessionInputSchema>;
|
|
14
18
|
export type AssignSessionTicketsInput = z.infer<typeof assignSessionTicketsInputSchema>;
|
|
19
|
+
export type JoinSessionInput = z.infer<typeof joinSessionSchema>;
|
|
20
|
+
export type BulkCreateGameTurnInput = z.infer<typeof bulkCreateGameTurnSchema>;
|
|
@@ -43,6 +43,8 @@ export const purchaseSchema = z.object({
|
|
|
43
43
|
|
|
44
44
|
// Buyer can be null for Guest/On-Site purchases
|
|
45
45
|
buyerUserId: z.string().optional().nullable(),
|
|
46
|
+
guestName: z.string().optional().nullable(),
|
|
47
|
+
guestContact: z.string().optional().nullable(),
|
|
46
48
|
|
|
47
49
|
items: z.array(purchaseItemSchema),
|
|
48
50
|
totalAmountCents: z.number().min(0),
|
|
@@ -59,6 +61,8 @@ export const purchaseSchema = z.object({
|
|
|
59
61
|
|
|
60
62
|
export const createPurchaseInputSchema = z.object({
|
|
61
63
|
buyerUserId: z.string().optional(),
|
|
64
|
+
guestName: z.string().optional(),
|
|
65
|
+
guestContact: z.string().optional(),
|
|
62
66
|
items: z.array(purchaseItemSchema).min(1),
|
|
63
67
|
});
|
|
64
68
|
|
|
@@ -82,6 +86,7 @@ export const gameTurnTaskSchema = z.object({
|
|
|
82
86
|
export const gameTurnSchema = z.object({
|
|
83
87
|
id: z.string(),
|
|
84
88
|
purchaseId: z.string(),
|
|
89
|
+
sessionId: z.string().optional(),
|
|
85
90
|
leaderPlayerId: z.string(),
|
|
86
91
|
gameTickets: z.array(z.string()),
|
|
87
92
|
playerIds: z.array(z.string()),
|