! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-10.33.3.tgz

> my-v0-project@0.1.0 pretypecheck /home/sebas/ballbox
> rm -rf .next/types .next/dev/types

> my-v0-project@0.1.0 typecheck /home/sebas/ballbox
> tsc --noEmit --incremental false

lib/club-accounts-backend.ts(323,3): error TS2353: Object literal may only specify known properties, and 'coachPriceOverrides' does not exist in type 'VenueSelect<DefaultArgs>'.
lib/club-accounts-backend.ts(498,52): error TS2339: Property 'map' does not exist on type 'never'.
lib/club-accounts-backend.ts(498,57): error TS7006: Parameter 'override' implicitly has an 'any' type.
lib/club-accounts-backend.ts(1161,7): error TS2353: Object literal may only specify known properties, and 'assignedCourt' does not exist in type 'CoachSelect<DefaultArgs>'.
lib/club-accounts-backend.ts(1189,22): error TS2345: Argument of type '(coach: { id: string; name: string; email: string | null; clubId: string | null; venue: BallboxClubVenueSummary | null; assignedCourt?: BallboxClubCourtSummary | null | undefined; availabilities?: { ...; }[] | undefined; _count: { ...; }; classOffers: { ...; }[]; }) => BallboxClubRosterCoach' is not assignable to parameter of type '(value: { name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }, index: number, array: { ...; }[]) => BallboxClubRosterCoach'.
  Types of parameters 'coach' and 'value' are incompatible.
    Type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is missing the following properties from type '{ id: string; name: string; email: string | null; clubId: string | null; venue: BallboxClubVenueSummary | null; assignedCourt?: BallboxClubCourtSummary | null | undefined; availabilities?: { ...; }[] | undefined; _count: { ...; }; classOffers: { ...; }[]; }': venue, _count, classOffers
lib/club-accounts-backend.ts(1903,16): error TS2339: Property 'venueCoachPriceOverride' does not exist on type 'Omit<PrismaClient<PrismaClientOptions, never, DefaultArgs>, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends">'.
lib/club-accounts-backend.ts(1906,18): error TS2339: Property 'venueCoachPriceOverride' does not exist on type 'Omit<PrismaClient<PrismaClientOptions, never, DefaultArgs>, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends">'.
lib/club-accounts-backend.ts(1972,16): error TS2353: Object literal may only specify known properties, and 'assignedCourtId' does not exist in type 'CoachWhereInput | CoachWhereInput[]'.
lib/club-accounts-backend.ts(1982,9): error TS2353: Object literal may only specify known properties, and 'assignedCourtId' does not exist in type '(Without<CoachUpdateManyMutationInput, CoachUncheckedUpdateManyInput> & CoachUncheckedUpdateManyInput) | (Without<...> & CoachUpdateManyMutationInput)'.
lib/club-accounts-backend.ts(2030,22): error TS2353: Object literal may only specify known properties, and 'assignedCourtId' does not exist in type 'Without<CoachUpdateInput, CoachUncheckedUpdateInput> & CoachUncheckedUpdateInput'.
lib/club-accounts-backend.ts(2042,7): error TS2353: Object literal may only specify known properties, and 'assignedCourt' does not exist in type 'CoachSelect<DefaultArgs>'.
lib/club-accounts-backend.ts(2068,52): error TS2345: Argument of type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is not assignable to parameter of type '{ id: string; name: string; email: string | null; clubId: string | null; venue: BallboxClubVenueSummary | null; assignedCourt?: BallboxClubCourtSummary | null | undefined; availabilities?: { ...; }[] | undefined; _count: { ...; }; classOffers: { ...; }[]; }'.
  Type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is missing the following properties from type '{ id: string; name: string; email: string | null; clubId: string | null; venue: BallboxClubVenueSummary | null; assignedCourt?: BallboxClubCourtSummary | null | undefined; availabilities?: { ...; }[] | undefined; _count: { ...; }; classOffers: { ...; }[]; }': venue, _count, classOffers
lib/club-accounts-backend.ts(2082,42): error TS2353: Object literal may only specify known properties, and 'assignedCourtId' does not exist in type 'Without<CoachUpdateInput, CoachUncheckedUpdateInput> & CoachUncheckedUpdateInput'.
lib/club-accounts-backend.ts(2094,7): error TS2353: Object literal may only specify known properties, and 'assignedCourt' does not exist in type 'CoachSelect<DefaultArgs>'.
lib/club-accounts-backend.ts(2120,52): error TS2345: Argument of type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is not assignable to parameter of type '{ id: string; name: string; email: string | null; clubId: string | null; venue: BallboxClubVenueSummary | null; assignedCourt?: BallboxClubCourtSummary | null | undefined; availabilities?: { ...; }[] | undefined; _count: { ...; }; classOffers: { ...; }[]; }'.
  Type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is missing the following properties from type '{ id: string; name: string; email: string | null; clubId: string | null; venue: BallboxClubVenueSummary | null; assignedCourt?: BallboxClubCourtSummary | null | undefined; availabilities?: { ...; }[] | undefined; _count: { ...; }; classOffers: { ...; }[]; }': venue, _count, classOffers
lib/coach-students-backend.ts(441,65): error TS2741: Property 'paymentStatus' is missing in type '{ venue: { name: string; club: { name: string | null; id: string; }; id: string; }; player: { memberships: ({ club: { name: string | null; id: string; }; } & { id: string; clubId: string; createdAt: Date; updatedAt: Date; playerId: string; })[]; } & { ...; }; court: { ...; } | null; } & { ...; }' but required in type '{ id: string; startsAt: Date; endsAt: Date; courtPriceMinor: number | null; coachPriceMinor: number | null; status: "SCHEDULED" | "CANCELED"; paymentStatus: "PENDING" | "PAID"; note: string | null; player: { ...; }; venue: { ...; }; court: { ...; } | null; }'.
lib/coach-students-backend.ts(687,24): error TS2741: Property 'paymentStatus' is missing in type '{ venue: { name: string; club: { name: string | null; id: string; }; id: string; }; player: { memberships: ({ club: { name: string | null; id: string; }; } & { id: string; clubId: string; createdAt: Date; updatedAt: Date; playerId: string; })[]; } & { ...; }; court: { ...; } | null; } & { ...; }' but required in type '{ id: string; startsAt: Date; endsAt: Date; courtPriceMinor: number | null; coachPriceMinor: number | null; status: "SCHEDULED" | "CANCELED"; paymentStatus: "PENDING" | "PAID"; note: string | null; player: { ...; }; venue: { ...; }; court: { ...; } | null; }'.
lib/coach-students-backend.ts(776,36): error TS2345: Argument of type '{ memberships: ({ club: { name: string | null; id: string; }; } & { id: string; clubId: string; createdAt: Date; updatedAt: Date; playerId: string; })[]; } & { name: string; id: string; createdAt: Date; updatedAt: Date; whatsappPhone: string | null; telegramHandle: string | null; }' is not assignable to parameter of type '{ id: string; name: string; whatsappPhone: string | null; telegramHandle: string | null; gender: "FEMALE" | "MALE" | "OTHER" | null; birthYear: number | null; coachCategory: string | null; memberships: { ...; }[]; }'.
  Type '{ memberships: ({ club: { name: string | null; id: string; }; } & { id: string; clubId: string; createdAt: Date; updatedAt: Date; playerId: string; })[]; } & { name: string; id: string; createdAt: Date; updatedAt: Date; whatsappPhone: string | null; telegramHandle: string | null; }' is missing the following properties from type '{ id: string; name: string; whatsappPhone: string | null; telegramHandle: string | null; gender: "FEMALE" | "MALE" | "OTHER" | null; birthYear: number | null; coachCategory: string | null; memberships: { ...; }[]; }': gender, birthYear, coachCategory
lib/coaches-backend.ts(698,7): error TS2353: Object literal may only specify known properties, and 'assignedCourt' does not exist in type 'CoachSelect<DefaultArgs>'.
lib/coaches-backend.ts(769,7): error TS2353: Object literal may only specify known properties, and 'assignedCourt' does not exist in type 'CoachSelect<DefaultArgs>'.
lib/coaches-backend.ts(774,26): error TS2345: Argument of type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is not assignable to parameter of type '{ id: string; name: string; email: string | null; clubSelectionMode: "ALL" | "CUSTOM"; club: { id: string; name: string | null; } | null; venue: { id: string; name: string; } | null; assignedCourt: { ...; } | null; }'.
  Type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is missing the following properties from type '{ id: string; name: string; email: string | null; clubSelectionMode: "ALL" | "CUSTOM"; club: { id: string; name: string | null; } | null; venue: { id: string; name: string; } | null; assignedCourt: { ...; } | null; }': club, venue, assignedCourt
lib/coaches-backend.ts(788,7): error TS2353: Object literal may only specify known properties, and 'assignedCourt' does not exist in type 'CoachSelect<DefaultArgs>'.
lib/coaches-backend.ts(793,32): error TS2345: Argument of type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is not assignable to parameter of type '{ id: string; name: string; email: string | null; passwordHash: string | null; clubSelectionMode: "ALL" | "CUSTOM"; club: { id: string; name: string | null; } | null; venue: { id: string; name: string; } | null; assignedCourt: { ...; } | null; }'.
  Type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is missing the following properties from type '{ id: string; name: string; email: string | null; passwordHash: string | null; clubSelectionMode: "ALL" | "CUSTOM"; club: { id: string; name: string | null; } | null; venue: { id: string; name: string; } | null; assignedCourt: { ...; } | null; }': club, venue, assignedCourt
lib/coaches-backend.ts(806,7): error TS2353: Object literal may only specify known properties, and 'assignedCourt' does not exist in type 'CoachSelect<DefaultArgs>'.
lib/coaches-backend.ts(811,26): error TS2345: Argument of type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is not assignable to parameter of type '{ id: string; name: string; email: string | null; clubSelectionMode: "ALL" | "CUSTOM"; club: { id: string; name: string | null; } | null; venue: { id: string; name: string; } | null; assignedCourt: { ...; } | null; }'.
  Type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is missing the following properties from type '{ id: string; name: string; email: string | null; clubSelectionMode: "ALL" | "CUSTOM"; club: { id: string; name: string | null; } | null; venue: { id: string; name: string; } | null; assignedCourt: { ...; } | null; }': club, venue, assignedCourt
lib/coaches-backend.ts(910,9): error TS2353: Object literal may only specify known properties, and 'assignedCourtId' does not exist in type 'Without<CoachUpdateInput, CoachUncheckedUpdateInput> & CoachUncheckedUpdateInput'.
lib/coaches-backend.ts(1118,7): error TS2353: Object literal may only specify known properties, and 'assignedCourt' does not exist in type 'CoachSelect<DefaultArgs>'.
lib/coaches-backend.ts(1135,23): error TS2339: Property 'club' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'.
lib/coaches-backend.ts(1141,17): error TS2551: Property 'clubSelections' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'clubSelectionMode'?
lib/coaches-backend.ts(1141,37): error TS7006: Parameter 'selection' implicitly has an 'any' type.
lib/coaches-backend.ts(1141,77): error TS7006: Parameter 'clubId' implicitly has an 'any' type.
lib/coaches-backend.ts(1146,34): error TS2339: Property 'club' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'.
lib/coaches-backend.ts(1147,44): error TS2339: Property 'assignedCourt' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'.
lib/coaches-backend.ts(1148,20): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1150,23): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1151,25): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1152,40): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1153,42): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1154,49): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1155,49): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1156,43): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1157,31): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1158,27): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1195,35): error TS2345: Argument of type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is not assignable to parameter of type '{ id: string; name: string; email: string | null; clubSelectionMode: "ALL" | "CUSTOM"; club: { id: string; name: string | null; } | null; venue: { id: string; name: string; } | null; assignedCourt: { ...; } | null; }'.
  Type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }' is missing the following properties from type '{ id: string; name: string; email: string | null; clubSelectionMode: "ALL" | "CUSTOM"; club: { id: string; name: string | null; } | null; venue: { id: string; name: string; } | null; assignedCourt: { ...; } | null; }': club, venue, assignedCourt
lib/coaches-backend.ts(1204,17): error TS2551: Property 'clubSelections' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'clubSelectionMode'?
lib/coaches-backend.ts(1204,37): error TS7006: Parameter 'item' implicitly has an 'any' type.
lib/coaches-backend.ts(1204,67): error TS7006: Parameter 'clubId' implicitly has an 'any' type.
lib/coaches-backend.ts(1209,34): error TS2339: Property 'club' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'.
lib/coaches-backend.ts(1210,44): error TS2339: Property 'assignedCourt' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'.
lib/coaches-backend.ts(1211,20): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1213,23): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1214,25): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1215,40): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1216,42): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1217,49): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1218,49): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1219,43): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1220,31): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1221,27): error TS2551: Property 'venue' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'. Did you mean 'venueId'?
lib/coaches-backend.ts(1228,26): error TS2339: Property 'salesPackages' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'.
lib/coaches-backend.ts(1231,24): error TS2339: Property 'classOffers' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'.
lib/coaches-backend.ts(1233,30): error TS2339: Property 'availabilities' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'.
lib/coaches-backend.ts(1246,20): error TS2339: Property 'availabilities' does not exist on type '{ name: string; email: string | null; id: string; clubId: string | null; createdAt: Date; updatedAt: Date; passwordHash: string | null; mercadoPagoHandle: string | null; venueId: string | null; clubSelectionMode: CoachClubSelectionMode; }'.
lib/players-backend.ts(105,22): error TS2339: Property 'gender' does not exist on type '{ memberships: ({ club: { id: string; atcSportclubId: string | null; }; } & { id: string; clubId: string; createdAt: Date; updatedAt: Date; playerId: string; })[]; } & { name: string; id: string; createdAt: Date; updatedAt: Date; whatsappPhone: string | null; telegramHandle: string | null; }'.
lib/players-backend.ts(105,38): error TS2339: Property 'gender' does not exist on type '{ memberships: ({ club: { id: string; atcSportclubId: string | null; }; } & { id: string; clubId: string; createdAt: Date; updatedAt: Date; playerId: string; })[]; } & { name: string; id: string; createdAt: Date; updatedAt: Date; whatsappPhone: string | null; telegramHandle: string | null; }'.
lib/players-backend.ts(106,25): error TS2339: Property 'birthYear' does not exist on type '{ memberships: ({ club: { id: string; atcSportclubId: string | null; }; } & { id: string; clubId: string; createdAt: Date; updatedAt: Date; playerId: string; })[]; } & { name: string; id: string; createdAt: Date; updatedAt: Date; whatsappPhone: string | null; telegramHandle: string | null; }'.
lib/players-backend.ts(107,29): error TS2339: Property 'coachCategory' does not exist on type '{ memberships: ({ club: { id: string; atcSportclubId: string | null; }; } & { id: string; clubId: string; createdAt: Date; updatedAt: Date; playerId: string; })[]; } & { name: string; id: string; createdAt: Date; updatedAt: Date; whatsappPhone: string | null; telegramHandle: string | null; }'.
lib/players-backend.ts(165,44): error TS2345: Argument of type '{ memberships: ({ club: { id: string; atcSportclubId: string | null; }; } & { id: string; clubId: string; createdAt: Date; updatedAt: Date; playerId: string; })[]; } & { name: string; id: string; createdAt: Date; updatedAt: Date; whatsappPhone: string | null; telegramHandle: string | null; }' is not assignable to parameter of type '{ id: string; name: string; whatsappPhone: string | null; telegramHandle: string | null; gender: "FEMALE" | "MALE" | "OTHER" | null; birthYear: number | null; coachCategory: string | null; memberships: { ...; }[]; }'.
  Type '{ memberships: ({ club: { id: string; atcSportclubId: string | null; }; } & { id: string; clubId: string; createdAt: Date; updatedAt: Date; playerId: string; })[]; } & { name: string; id: string; createdAt: Date; updatedAt: Date; whatsappPhone: string | null; telegramHandle: string | null; }' is missing the following properties from type '{ id: string; name: string; whatsappPhone: string | null; telegramHandle: string | null; gender: "FEMALE" | "MALE" | "OTHER" | null; birthYear: number | null; coachCategory: string | null; memberships: { ...; }[]; }': gender, birthYear, coachCategory
lib/players-backend.ts(180,20): error TS2345: Argument of type '{ memberships: ({ club: { id: string; atcSportclubId: string | null; }; } & { id: string; clubId: string; createdAt: Date; updatedAt: Date; playerId: string; })[]; } & { name: string; id: string; createdAt: Date; updatedAt: Date; whatsappPhone: string | null; telegramHandle: string | null; }' is not assignable to parameter of type '{ id: string; name: string; whatsappPhone: string | null; telegramHandle: string | null; gender: "FEMALE" | "MALE" | "OTHER" | null; birthYear: number | null; coachCategory: string | null; memberships: { ...; }[]; }'.
  Type '{ memberships: ({ club: { id: string; atcSportclubId: string | null; }; } & { id: string; clubId: string; createdAt: Date; updatedAt: Date; playerId: string; })[]; } & { name: string; id: string; createdAt: Date; updatedAt: Date; whatsappPhone: string | null; telegramHandle: string | null; }' is missing the following properties from type '{ id: string; name: string; whatsappPhone: string | null; telegramHandle: string | null; gender: "FEMALE" | "MALE" | "OTHER" | null; birthYear: number | null; coachCategory: string | null; memberships: { ...; }[]; }': gender, birthYear, coachCategory
lib/players-backend.ts(196,7): error TS2353: Object literal may only specify known properties, and 'gender' does not exist in type '(Without<PlayerCreateInput, PlayerUncheckedCreateInput> & PlayerUncheckedCreateInput) | (Without<...> & PlayerCreateInput)'.
prisma/seed.ts(475,16): error TS2339: Property 'venueCoachPriceOverride' does not exist on type 'PrismaClient<PrismaClientOptions, never, DefaultArgs>'.
prisma/seed.ts(486,18): error TS2339: Property 'venueCoachPriceOverride' does not exist on type 'PrismaClient<PrismaClientOptions, never, DefaultArgs>'.
prisma/seed.ts(549,9): error TS2353: Object literal may only specify known properties, and 'gender' does not exist in type '(Without<PlayerUpdateInput, PlayerUncheckedUpdateInput> & PlayerUncheckedUpdateInput) | (Without<...> & PlayerUpdateInput)'.
prisma/seed.ts(558,9): error TS2353: Object literal may only specify known properties, and 'gender' does not exist in type '(Without<PlayerCreateInput, PlayerUncheckedCreateInput> & PlayerUncheckedCreateInput) | (Without<...> & PlayerCreateInput)'.
 ELIFECYCLE  Command failed with exit code 2.

Command exited with code 2


! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-10.33.3.tgz

> my-v0-project@0.1.0 lint /home/sebas/ballbox
> eslint .


diff --git a/PLAN.md b/PLAN.md
index 62ebfd0..78b4b8b 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -31,6 +31,8 @@
 - Payments now also expose `POST /api/integrations/tcn/payments/init` as the first machine-facing TCN adapter over the same shared machine-payment flow used by the admin QR harness.
 - `/admin/payments` now also generates live MercadoPago QR orders for arbitrary amounts or per-product prices, with money inputs expressed in major units (for example `15.00` or `1000.00`), then shows the QR preview/order id for scan-and-pay validation.
 - That admin QR surface is now positioned as a provider test/debug harness; production vending-machine payments should integrate through the TCN machine protocol and let the machine render the buyer-facing QR.
+- `OPS-1` validation is defined as one real production payment where Ballbox creates the order, Mercado Pago approves it, the public webhook hits `/notification/mercadopago`, Ballbox fetches the provider order, persists a canonical `PaymentSession`, and that session is visible in `/admin/payments` with evidence saved.
+- Even after `OPS-1` passes, machine-launch confidence still depends on TCN-side proof for `FunCode=8000` request/response handling, QR rendering from `CodeUrl`, approval-to-dispense signaling, and retry/timeout/duplicate behavior under real machine traffic.
 - Club revenue share is currently handled with scheduled manual settlements/bookkeeping; Mercado Pago native split stays deferred until the machine flow and club count justify it.
 - `VendingMachine` now carries machine-payment ownership/config fields plus optional `externalMachineId`, and Ballbox now persists pre-approval `PaymentOperation` rows so TCN/admin order creation and webhook-side provider resolution share one source of truth.
 - Legacy approved-callback compatibility has been removed from operations; `/api/integrations/payments/approved` now returns `410` so only the native MercadoPago path remains active.
@@ -158,7 +160,7 @@
 - TVs errors now use a shared strict shape and the contract is validated by `pnpm test:contracts`.
 - Payments integration health and sessions routes now report Ballbox-internal native MercadoPago status plus webhook/order metadata.
 - Payments native MercadoPago webhook flow now lands in Ballbox, validates canonical payment correlation from fetched orders, and persists approved payments as `PaymentSession` records.
-- QR order creation now sets a public `notification_url` so Mercado Pago routes order updates back to Ballbox explicitly.
+- For the current Ballbox Mercado Pago account/API surface, QR order creation now relies on the account-level webhook configuration instead of sending inline `notification_url`, because Mercado Pago rejects that field on `POST /v1/orders` for this setup.
 - Unsupported merchant-order notification topics are ignored so only QR order events drive persistence.
 - Replay testing with two older live `ORD...` ids returned `MERCADOPAGO_ORDER_NOT_APPROVED_expired`; see `docs/mercadopago-webhook-replay-notes-2026-04-23.md` for the current blocker and next resume point.
 - Payments now have an admin CRUD/reconciliation surface over local `PaymentSession` rows via `/api/admin/payments/*`.
@@ -217,24 +219,27 @@
 ## Ordered Payments Track
 1. Finish provider-level Mercado Pago proof without TCN hardware.
    - backlog: `OPS-1` in `docs/next-steps-queue.md`
+   - done condition: one real production order created by Ballbox, one real approved Mercado Pago payment, public webhook delivery to `/notification/mercadopago`, Ballbox fetch-and-persist of the approved order, and visible reconciliation evidence in `/admin/payments`
    - docs: `docs/implemented-payments.md`, `docs/mercadopago-webhook-replay-notes-2026-04-23.md`
-2. Ask TCN sales to confirm the remaining machine-contract questions with their engineers.
+2. After `OPS-1`, treat provider risk as materially reduced but not machine risk.
+   - still required for machine confidence: real TCN `8000` request validation, `CodeUrl` rendering confirmation, approval-to-dispense handshake proof, duplicate/retry handling, timeout/expiry handling, and an operator runbook for approved-but-no-vend incidents
+3. Ask TCN sales to confirm the remaining machine-contract questions with their engineers.
    - docs: `docs/tcn-payments-open-questions.md`, `docs/tcn-payments-integration-plan.md`
    - note: this is required because the machines are not here yet
-3. Freeze the Ballbox <-> TCN adapter contract once those answers come back.
+4. Freeze the Ballbox <-> TCN adapter contract once those answers come back.
    - docs: `docs/tcn-payments-integration-plan.md`, `docs/tcn-payments-execution-plan.md`
-4. Implement one machine payment flow with two owner modes.
+5. Implement one machine payment flow with two owner modes.
    - `BALLBOX`: Ballbox-collected club machines
    - `TENANT`: tenant/client-collected machines such as Adidas rentals
    - backlog: `PAY-3` in `docs/next-steps-queue.md`
    - doc: `docs/payments-machine-rollout-rfc.md`
-5. Use guided onboarding for the first wave of 5 machines.
+6. Use guided onboarding for the first wave of 5 machines.
    - first wave target: 3 `BALLBOX` machines + 2 `TENANT` machines
    - docs: `docs/payments-machine-rollout-rfc.md`, `docs/tcn-payments-execution-plan.md`
-6. Keep club settlements informal/manual for Ballbox-collected machines until the machine path is stable and settlement pain is real.
+7. Keep club settlements informal/manual for Ballbox-collected machines until the machine path is stable and settlement pain is real.
    - backlog: `PAY-4` in `docs/next-steps-queue.md`
    - doc: `docs/payments-club-settlement-strategy.md`
-7. Revisit automation later only if volume or ops pain justify it.
+8. Revisit automation later only if volume or ops pain justify it.
    - current future candidate: Mercado Pago split / marketplace

 ### Payments Track Docs
diff --git a/docs/implemented-payments.md b/docs/implemented-payments.md
index 7c55bf6..66f53ff 100644
--- a/docs/implemented-payments.md
+++ b/docs/implemented-payments.md
@@ -34,7 +34,7 @@
 ## Integration Contract
 - Ballbox native MercadoPago webhook path is `/notification/mercadopago`.
 - Ballbox native QR order creation path is `/api/integrations/payments/orders`.
-- That route now also attaches a public `notification_url` so Mercado Pago sends QR order updates back to `/notification/mercadopago`.
+- For the current Ballbox Mercado Pago account/API surface, QR order create relies on the account-level webhook configuration instead of sending inline `notification_url` in the order payload, because Mercado Pago rejects that field on `POST /v1/orders` for this setup.
 - That route accepts an explicit amount for arbitrary charges or falls back to the Ballbox `Product.priceMinor` when the amount is omitted, which powers per-product QR generation in the admin UI.
 - The order route rejects values below Mercado Pago's minimum charge (`15.00 ARS`) before calling the provider.
 - QR webhook persistence now depends on the upstream order fetch plus canonical-reference mapping; merchant-order topic events are ignored.
diff --git a/docs/mercadopago-webhook-replay-notes-2026-04-23.md b/docs/mercadopago-webhook-replay-notes-2026-04-23.md
index 71a0576..632fb49 100644
--- a/docs/mercadopago-webhook-replay-notes-2026-04-23.md
+++ b/docs/mercadopago-webhook-replay-notes-2026-04-23.md
@@ -27,3 +27,17 @@ Date: 2026-04-23
 ## Recommended next step
 - Keep the webhook path as-is.
 - When resuming, use a fresh approved order id from a recent live payment or the buyer app flow.
+
+## Production order-create blocker (2026-05-05)
+- Attempted a new production QR order create against `https://ballbox.app/api/integrations/payments/orders` before the real buyer-payment step.
+- Result: Ballbox returned provider failure details from Mercado Pago:
+  - HTTP `400`
+  - code: `unsupported_properties`
+  - details: `additionalProperties '$.notification_url' not allowed`
+- Meaning:
+  - the Ballbox production account/API surface currently rejects the inline `notification_url` field used by the native order-create flow
+  - `OPS-1` is blocked before payment approval, not at webhook persistence
+- Next unblock:
+  - stop sending inline `notification_url` from Ballbox order-create
+  - rely on the Mercado Pago account-level webhook configuration already associated with the production setup
+  - then rerun `OPS-1` with a fresh real payment
diff --git a/docs/next-steps-queue.md b/docs/next-steps-queue.md
index 4de263f..a121238 100644
--- a/docs/next-steps-queue.md
+++ b/docs/next-steps-queue.md
@@ -72,6 +72,7 @@
 - Progress (2026-04-14): payments migration to host app started in one feature branch; health/sessions now expose internal MercadoPago mode, Ballbox ships `/notification/mercadopago` intake with order fetch verification, and native order creation now exists at `/api/integrations/payments/orders`.
 - Progress (2026-04-19): PAY-2 cleanup removed bridge-oriented operational status from host-app health/docs; OPS-1 is now focused on validating the native MercadoPago order-create + webhook-persist path only.
 - Progress (2026-04-23): TCN protocol review shows the `additional hardware devices` note is about machine-side QR scanning, while the machine payment flow (`FunCode=8000`) already returns `CodeUrl` for machine-screen QR display. This means the admin QR page should be treated as a provider test harness, and production integration should move to a TCN adapter layer.
+- Progress (2026-05-05): first production order-create attempt against `https://ballbox.app/api/integrations/payments/orders` failed before buyer payment with Mercado Pago `400 unsupported_properties` because `notification_url` was rejected (`additionalProperties '$.notification_url' not allowed`). Ballbox should stop sending inline `notification_url` for this account/API surface and rely on the existing account-level Mercado Pago webhook configuration before rerunning the real-payment check.
 - Expected payoff: closes highest-risk integration uncertainty

 ### OPS-2 TVs runtime parity closure
diff --git a/lib/payments-machine-flow.ts b/lib/payments-machine-flow.ts
index 1f228f1..a8b1589 100644
--- a/lib/payments-machine-flow.ts
+++ b/lib/payments-machine-flow.ts
@@ -3,7 +3,6 @@ import { prisma } from "@/lib/prisma";
 import { buildPaymentExternalReference, getPaymentProductDefaultPriceMinor } from "@/lib/payments";
 import { createMercadoPagoQrOrder, type MercadoPagoConfig, fetchMercadoPagoOrder } from "@/lib/payments-mercadopago";
 import { resolveMercadoPagoConfigForOwner, type PaymentOwnerMode } from "@/lib/payments-mercadopago-config";
-import { getPublicAppUrl } from "@/lib/public-url";

 export function buildMachineTxnId() {
   return `txn_${Date.now()}_${crypto.randomUUID().slice(0, 8)}`;
@@ -83,10 +82,6 @@ export async function createMachinePaymentOrder(input: {
     return { ok: false as const, error: "INVALID_PAYLOAD", status: 400, details: { field: "externalPosId" } };
   }

-  const notificationUrl = getPublicAppUrl()
-    ? `${getPublicAppUrl()}/notification/mercadopago?source_news=webhooks`
-    : undefined;
-
   const description = input.description?.trim() || `${product.name} (${context.vendingMachine.label || context.vendingMachine.id})`;

   const existing = await prisma.paymentOperation.findUnique({
@@ -167,7 +162,6 @@ export async function createMachinePaymentOrder(input: {
       mode: input.mode,
       description,
       expirationTime: input.expirationTime,
-      notificationUrl,
     },
     context.config as MercadoPagoConfig
   );
diff --git a/lib/payments-mercadopago.ts b/lib/payments-mercadopago.ts
index b1f7ce3..55b225f 100644
--- a/lib/payments-mercadopago.ts
+++ b/lib/payments-mercadopago.ts
@@ -206,7 +206,6 @@ export async function createMercadoPagoQrOrder(input: {
   mode: "static" | "dynamic" | "hybrid";
   description: string;
   expirationTime: string;
-  notificationUrl?: string;
 }, config: MercadoPagoConfig = getMercadoPagoConfig()) {

   if (!config.accessToken) {
@@ -255,11 +254,6 @@ export async function createMercadoPagoQrOrder(input: {
           external_categories: [{ id: "ballbox-product" }],
         },
       ],
-      ...(input.notificationUrl
-        ? {
-            notification_url: input.notificationUrl,
-          }
-        : {}),
     }),
   });


**Switching branches in Git**

I need to run a targeted proof directly with the provider included, but creating a local script might not be necessary. I should consider switching to the main branch and committing any changes. It might help to check the git status before I commit. However, since the user asked me to go directly to the main branch, I should probably perform a checkout first. It seems there might be unrelated changes in the branch, so switching makes sense.

bash

git status --short && echo '---' && git checkout main


 M PLAN.md
 M docs/implemented-payments.md
 M docs/mercadopago-webhook-replay-notes-2026-04-23.md
 M docs/next-steps-queue.md
 M lib/payments-machine-flow.ts
 M lib/payments-mercadopago.ts
---
error: Your local changes to the following files would be overwritten by checkout:
	PLAN.md
	docs/implemented-payments.md
	docs/next-steps-queue.md
	lib/payments-machine-flow.ts
	lib/payments-mercadopago.ts
Please commit your changes or stash them before you switch branches.
Aborting

Command exited with code 1


**Planning version control tasks**

I need to make a commit on the current branch first. After that, I’ll switch to the main branch and use cherry-pick to pull in the specific changes I want. It’s a pretty straightforward process, but I want to make sure I do it in the right order to avoid any issues or complications down the line. Okay, let’s get this done!

git add PLAN.md docs/implemented-payments.md docs/mercadopago-webhook-replay-notes-2026-04-23.md docs/next-steps-queue.md lib/payments-machine-flow.ts lib/payments-mercadopago.ts && git commit -m "Fix Mercado Pago QR order create payload"


[pay-3-tcn-adapter 2e07bb1] Fix Mercado Pago QR order create payload
 6 files changed, 28 insertions(+), 20 deletions(-)


**Checking file status**

I see that I've only committed 6 files. It looks like there might be some replay notes that are staged but not committed. The commit message confirms the 6 files, but it makes me wonder if I’ve missed anything. I definitely need to check the status to clear up my confusion and ensure everything is in order. It's always good to double-check, just to be safe!

git status --short && git show --stat --oneline --name-only HEAD


2e07bb1 Fix Mercado Pago QR order create payload
PLAN.md
docs/implemented-payments.md
docs/mercadopago-webhook-replay-notes-2026-04-23.md
docs/next-steps-queue.md
lib/payments-machine-flow.ts
lib/payments-mercadopago.ts


git checkout main && git cherry-pick 2e07bb1


Switched to branch 'main'
Your branch is behind 'origin/main' by 15 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)
Auto-merging PLAN.md
Auto-merging docs/implemented-payments.md
Auto-merging docs/next-steps-queue.md
CONFLICT (modify/delete): lib/payments-machine-flow.ts deleted in HEAD and modified in 2e07bb1 (Fix Mercado Pago QR order create payload).  Version 2e07bb1 (Fix Mercado Pago QR order create payload) of lib/payments-machine-flow.ts left in tree.
Auto-merging lib/payments-mercadopago.ts
CONFLICT (content): Merge conflict in lib/payments-mercadopago.ts
error: could not apply 2e07bb1... Fix Mercado Pago QR order create payload
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".
hint: Disable this message with "git config advice.mergeConflict false"

Command exited with code 1


**Resolving file layout issues**

I need to approach this carefully since the main file layout is different. I’ll inspect the status and look into any conflicting files. It's important to ensure I understand what’s going on here before making any changes. I want to avoid problems down the line, so taking my time to resolve this seems like the best way to go. Let's figure this out step by step!

git status --short && echo '--- CONFLICT mercadopago' && git diff -- lib/payments-mercadopago.ts && echo '--- FILE machine flow exists?' && ls lib/payments-machine-flow.ts lib/payments-mercadopago.ts

rg -n "createMachinePaymentOrder|notification_url|getPublicAppUrl|createMercadoPagoQrOrder\(" lib app | sed -n '1,220p'


M  PLAN.md
M  docs/implemented-payments.md
M  docs/mercadopago-webhook-replay-notes-2026-04-23.md
M  docs/next-steps-queue.md
DU lib/payments-machine-flow.ts
UU lib/payments-mercadopago.ts
--- CONFLICT mercadopago
diff --cc lib/payments-mercadopago.ts
index c4d7030,55b225f..0000000
--- a/lib/payments-mercadopago.ts
+++ b/lib/payments-mercadopago.ts
@@@ -207,9 -206,7 +207,13 @@@ export async function createMercadoPago
    mode: "static" | "dynamic" | "hybrid";
    description: string;
    expirationTime: string;
++<<<<<<< HEAD
 +  notificationUrl?: string;
 +}) {
 +  const config = getMercadoPagoConfig();
++=======
+ }, config: MercadoPagoConfig = getMercadoPagoConfig()) {
++>>>>>>> 2e07bb1 (Fix Mercado Pago QR order create payload)

    if (!config.accessToken) {
      return {
--- FILE machine flow exists?
lib/payments-machine-flow.ts
lib/payments-mercadopago.ts


  return {
    ok: true,
    value: {
      sessionId,
      vendingMachineId: correlation.parsedReference.vendingMachineId,
      productId,
      machineTxnId: correlation.parsedReference.machineTxnId,
      externalReference,
      amountMinor,
      currency: order.currency?.trim() || "ARS",
      status: "approved",
      approvedAt,
      lateApproval: false,
      providerPaymentId,
    },
  };
}

export async function createMercadoPagoQrOrder(input: {
  vendingMachineId: string;
  productId: string;
  amountMinor: number;
  currency: string;
  externalReference: string;
  machineTxnId: string;
  externalPosId: string;
  mode: "static" | "dynamic" | "hybrid";
  description: string;
  expirationTime: string;
<<<<<<< HEAD
  notificationUrl?: string;
}) {
  const config = getMercadoPagoConfig();
=======
}, config: MercadoPagoConfig = getMercadoPagoConfig()) {
>>>>>>> 2e07bb1 (Fix Mercado Pago QR order create payload)

  if (!config.accessToken) {
    return {
      ok: false as const,
      status: 500,
      error: "MERCADOPAGO_ACCESS_TOKEN_NOT_CONFIGURED",
    };
  }

  const response = await fetch(`${config.publicApiBaseUrl}/v1/orders`, {
    method: "POST",
    cache: "no-store",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.accessToken}`,
      "X-Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({
      type: "qr",
      total_amount: amountMinorToMercadoPagoAmount(input.amountMinor),
      description: input.description,
      external_reference: input.externalReference,
      expiration_time: input.expirationTime,
      config: {
        qr: {
          external_pos_id: input.externalPosId,
          mode: input.mode,
        },
      },
      transactions: {
        payments: [
          {
            amount: amountMinorToMercadoPagoAmount(input.amountMinor),
          },
        ],
      },
      items: [
        {
          title: input.description,
          unit_price: amountMinorToMercadoPagoAmount(input.amountMinor),
          quantity: 1,
          unit_measure: "unit",
          external_code: input.productId,
          external_categories: [{ id: "ballbox-product" }],
        },
      ],
    }),
  });

  const payload = (await response.json().catch(() => null)) as
    | {
        id?: string | number;
        external_reference?: string;
        status?: string;
        status_detail?: string;
        expiration_time?: string;
        type_response?: { qr_data?: string };
      }
    | null;

  if (!response.ok) {
    return {
      ok: false as const,
      status: response.status,
      error: `MERCADOPAGO_ORDER_CREATE_FAILED_${response.status}`,
      details: payload,
    };
  }

  return {
    ok: true as const,
    value: {
      orderId: payload?.id ? String(payload.id) : "",
      externalReference: payload?.external_reference ?? input.externalReference,
      vendingMachineId: input.vendingMachineId,
      productId: input.productId,
      machineTxnId: input.machineTxnId,
      amountMinor: input.amountMinor,
      currency: input.currency,
      mode: input.mode,
      externalPosId: input.externalPosId,
      status: payload?.status ?? "created",

[7 more lines in file. Use offset=300 to continue.]


app/api/integrations/payments/orders/route.ts:14:import { getPublicAppUrl } from "@/lib/public-url";
app/api/integrations/payments/orders/route.ts:92:  const publicAppUrl = getPublicAppUrl();
app/api/integrations/payments/orders/route.ts:95:  const createResult = await createMercadoPagoQrOrder({
app/api/integrations/payments/health/route.ts:2:import { getPublicAppUrl } from "@/lib/public-url";
app/api/integrations/payments/health/route.ts:6:  const publicAppUrl = getPublicAppUrl();
app/api/integrations/payments/sessions/route.ts:3:import { getPublicAppUrl } from "@/lib/public-url";
app/api/integrations/payments/sessions/route.ts:8:  const publicAppUrl = getPublicAppUrl();
lib/public-url.ts:1:export function getPublicAppUrl() {
lib/payments-machine-flow.ts:39:export async function createMachinePaymentOrder(input: {
lib/payments-machine-flow.ts:153:  const createResult = await createMercadoPagoQrOrder(
lib/payments-mercadopago.ts:199:export async function createMercadoPagoQrOrder(input: {


**Inspecting routes and flows**

I think there's a significant difference in how the main function operates compared to the app/api/orders route, which seems to use createMercadoPagoQrOrder directly, possibly incorporating a notification URL. I really need to dive in and inspect that route further to understand its machine-flow better. It feels important to piece together how these elements interact to ensure everything works smoothly. Let's clarify this process!


import crypto from "node:crypto";
import {
  paymentOrderCreateResponseSchema,
  paymentOrderCreateSchema,
} from "@/lib/contracts/payments";
import {
  parsePaymentsJsonBody,
  paymentsErrorResponse,
  paymentsInvalidPayloadResponse,
  paymentsJsonResponse,
} from "@/lib/http/payments-route-helpers";
import { createMercadoPagoQrOrder, getMercadoPagoConfig } from "@/lib/payments-mercadopago";
import { buildPaymentExternalReference, getPaymentProductDefaultPriceMinor } from "@/lib/payments";
import { getPublicAppUrl } from "@/lib/public-url";
import { prisma } from "@/lib/prisma";

function buildMachineTxnId() {
  return `txn_${Date.now()}_${crypto.randomUUID().slice(0, 8)}`;
}

export async function GET() {
  const config = getMercadoPagoConfig();

  return paymentsJsonResponse({
    ok: true,
    configured: {
      accessToken: Boolean(config.accessToken),
      webhookSecret: Boolean(config.webhookSecret),
      externalPosId: Boolean(config.externalPosId),
    },
    defaults: {
      mode: "dynamic",
      expirationTime: "PT15M",
      externalPosId: config.externalPosId,
    },
  });
}

export async function POST(request: Request) {
  const parsedBody = await parsePaymentsJsonBody(request, paymentOrderCreateSchema);
  if (!parsedBody.success) {
    return paymentsInvalidPayloadResponse(parsedBody.error);
  }

  const vendingMachine = await prisma.vendingMachine.findUnique({
    where: { id: parsedBody.data.vendingMachineId },
    select: { id: true, label: true },
  });
  if (!vendingMachine) {
    return paymentsErrorResponse("VENDING_MACHINE_NOT_FOUND", 404);
  }

  const product = await prisma.product.findUnique({
    where: { id: parsedBody.data.productId },
    select: { id: true, name: true },
  });
  if (!product) {
    return paymentsErrorResponse("PRODUCT_NOT_FOUND", 404);
  }

  const config = getMercadoPagoConfig();
  const externalPosId = parsedBody.data.externalPosId ?? config.externalPosId;
  if (!externalPosId) {
    return paymentsErrorResponse("INVALID_PAYLOAD", 400, {
      field: "externalPosId",
      message: "Missing external POS id in payload and BALLBOX_MERCADOPAGO_EXTERNAL_POS_ID",
    });
  }

  const machineTxnId = buildMachineTxnId();
  const externalReference = buildPaymentExternalReference({
    vendingMachineId: parsedBody.data.vendingMachineId,
    machineTxnId,
  });
  const amountMinor = parsedBody.data.amountMinor ?? getPaymentProductDefaultPriceMinor(product.id) ?? null;
  if (amountMinor === null) {
    return paymentsErrorResponse("INVALID_PAYLOAD", 400, {
      field: "amountMinor",
      message: "Missing amountMinor and product priceMinor",
    });
  }

  if (amountMinor < 1500) {
    return paymentsErrorResponse("INVALID_PAYLOAD", 400, {
      field: "amountMinor",
      message: "Amount must be at least 15.00 ARS",
    });
  }

  const description =
    parsedBody.data.description?.trim() || `${product.name} (${vendingMachine.label || vendingMachine.id})`;
  const publicAppUrl = getPublicAppUrl();
  const notificationUrl = publicAppUrl ? `${publicAppUrl}/notification/mercadopago?source_news=webhooks` : undefined;

  const createResult = await createMercadoPagoQrOrder({
    vendingMachineId: parsedBody.data.vendingMachineId,
    productId: parsedBody.data.productId,
    amountMinor,
    currency: parsedBody.data.currency,
    externalReference,
    machineTxnId,
    externalPosId,
    mode: parsedBody.data.mode,
    description,
    expirationTime: parsedBody.data.expirationTime,
    notificationUrl,
  });

  if (!createResult.ok) {
    return paymentsErrorResponse("PAYMENT_SESSION_CONFLICT", 502, {
      error: createResult.error,
      status: createResult.status,
      details: createResult.details,
    });
  }

  return paymentsJsonResponse(paymentOrderCreateResponseSchema.parse({ ok: true, item: createResult.value }), 201);
}


import crypto from "node:crypto";
import { prisma } from "@/lib/prisma";
import { buildPaymentExternalReference, getPaymentProductDefaultPriceMinor } from "@/lib/payments";
import { createMercadoPagoQrOrder, type MercadoPagoConfig, fetchMercadoPagoOrder } from "@/lib/payments-mercadopago";
import { resolveMercadoPagoConfigForOwner, type PaymentOwnerMode } from "@/lib/payments-mercadopago-config";

export function buildMachineTxnId() {
  return `txn_${Date.now()}_${crypto.randomUUID().slice(0, 8)}`;
}

export async function resolveMachinePaymentContext(input: { vendingMachineId?: string; externalMachineId?: string | null }) {
  const vendingMachine = await prisma.vendingMachine.findFirst({
    where: input.vendingMachineId
      ? { id: input.vendingMachineId }
      : { externalMachineId: input.externalMachineId ?? undefined },
    select: {
      id: true,
      label: true,
      externalMachineId: true,
      paymentOwnerMode: true,
      paymentOwnerKey: true,
      paymentExternalPosId: true,
    },
  });

  if (!vendingMachine) return null;

  const config = resolveMercadoPagoConfigForOwner({
    ownerMode: vendingMachine.paymentOwnerMode as PaymentOwnerMode,
    ownerKey: vendingMachine.paymentOwnerKey,
  });

  return {
    vendingMachine,
    config,
  };
}

export async function createMachinePaymentOrder(input: {
  vendingMachineId?: string;
  externalMachineId?: string | null;
  productId: string;
  amountMinor?: number | null;
  currency: string;
  mode: "static" | "dynamic" | "hybrid";
  description?: string | null;
  expirationTime: string;
  machineTxnId?: string | null;
  source: string;
  slotNo?: string | null;
  notifyUrl?: string | null;
}) {
  const context = await resolveMachinePaymentContext({
    vendingMachineId: input.vendingMachineId,
    externalMachineId: input.externalMachineId,
  });
  if (!context) {
    return { ok: false as const, error: "VENDING_MACHINE_NOT_FOUND", status: 404 };
  }

  const product = await prisma.product.findUnique({
    where: { id: input.productId },
    select: { id: true, name: true },
  });
  if (!product) {
    return { ok: false as const, error: "PRODUCT_NOT_FOUND", status: 404 };
  }

  const amountMinor = input.amountMinor ?? getPaymentProductDefaultPriceMinor(product.id) ?? null;
  if (amountMinor === null || amountMinor < 1500) {
    return { ok: false as const, error: "INVALID_PAYLOAD", status: 400, details: { field: "amountMinor" } };
  }

  const machineTxnId = input.machineTxnId?.trim() || buildMachineTxnId();
  const externalReference = buildPaymentExternalReference({
    vendingMachineId: context.vendingMachine.id,
    machineTxnId,
  });

  const externalPosId = context.vendingMachine.paymentExternalPosId ?? context.config.externalPosId;
  if (!externalPosId) {
    return { ok: false as const, error: "INVALID_PAYLOAD", status: 400, details: { field: "externalPosId" } };
  }

  const description = input.description?.trim() || `${product.name} (${context.vendingMachine.label || context.vendingMachine.id})`;

  const existing = await prisma.paymentOperation.findUnique({
    where: {
      vendingMachineId_machineTxnId: {
        vendingMachineId: context.vendingMachine.id,
        machineTxnId,
      },
    },
  });

  if (existing?.providerOrderId) {
    return {
      ok: true as const,
      value: {
        orderId: existing.providerOrderId,
        externalReference: existing.externalReference,
        vendingMachineId: existing.vendingMachineId,
        productId: existing.productId,
        machineTxnId: existing.machineTxnId,
        amountMinor: existing.amountMinor,
        currency: existing.currency,
        mode: input.mode,
        externalPosId: existing.externalPosId,
        status: existing.status.toLowerCase(),
        statusDetail: "reused-existing-operation",
        expiresAt: null,
        qrData: null,
      },
      operationId: existing.id,
      ownerMode: existing.paymentOwnerMode,
      ownerKey: existing.paymentOwnerKey,
    };
  }

  const operation = await prisma.paymentOperation.upsert({
    where: { externalReference },
    update: {
      vendingMachineId: context.vendingMachine.id,
      productId: product.id,
      machineTxnId,
      paymentOwnerMode: context.vendingMachine.paymentOwnerMode,
      paymentOwnerKey: context.vendingMachine.paymentOwnerKey,
      externalPosId,
      amountMinor,
      currency: input.currency,
      source: input.source,
      slotNo: input.slotNo ?? null,
      notifyUrl: input.notifyUrl?.trim() || null,
      rawPayload: input,
    },
    create: {
      vendingMachineId: context.vendingMachine.id,
      productId: product.id,
      machineTxnId,
      externalReference,
      paymentOwnerMode: context.vendingMachine.paymentOwnerMode,
      paymentOwnerKey: context.vendingMachine.paymentOwnerKey,
      externalPosId,
      amountMinor,
      currency: input.currency,
      source: input.source,
      slotNo: input.slotNo ?? null,
      notifyUrl: input.notifyUrl?.trim() || null,
      rawPayload: input,
    },
  });

  const createResult = await createMercadoPagoQrOrder(
    {
      vendingMachineId: context.vendingMachine.id,
      productId: product.id,
      amountMinor,
      currency: input.currency,
      externalReference,
      machineTxnId,
      externalPosId,
      mode: input.mode,
      description,
      expirationTime: input.expirationTime,
    },
    context.config as MercadoPagoConfig
  );

  if (!createResult.ok) {
    await prisma.paymentOperation.update({
      where: { id: operation.id },
      data: {
        status: "FAILED",
        rawPayload: {
          ...input,
          error: createResult.error,
          details: createResult.details ?? null,
        },
      },
    });

    return createResult;
  }

  await prisma.paymentOperation.update({
    where: { id: operation.id },
    data: {
      providerOrderId: createResult.value.orderId || null,
      status: "CREATED",
      rawPayload: {
        ...input,
        createResult: createResult.value,
      },
    },
  });

  return {
    ok: true as const,
    value: createResult.value,
    operationId: operation.id,
    ownerMode: context.vendingMachine.paymentOwnerMode,
    ownerKey: context.vendingMachine.paymentOwnerKey,
  };
}

export async function fetchMercadoPagoOrderForWebhook(orderId: string) {
  const operation = await prisma.paymentOperation.findFirst({
    where: { providerOrderId: orderId },
    orderBy: { createdAt: "desc" },
  });

  const config = operation
    ? resolveMercadoPagoConfigForOwner({
        ownerMode: operation.paymentOwnerMode as PaymentOwnerMode,
        ownerKey: operation.paymentOwnerKey,
      })
    : resolveMercadoPagoConfigForOwner({ ownerMode: "BALLBOX", ownerKey: "ballbox-default" });

  return {
    operation,

[20 more lines in file. Use offset=221 to continue.]


**Inspecting file status**

I think since the file exists, maybe it came from a cherry-pick on the other side. If the main branch truly deleted it but the app is still importing it, that seems impossible. It’s likely showing up as "DU" due to the file being added in the cherry-pick while it's still tracked in HEAD. I’ll want to check the file status using `git ls-files -u` to get clarity on this situation.


100644 1f228f190debd68c2ab26ffc8cd67acac02bb02c 1	lib/payments-machine-flow.ts
100644 a8b1589f2888f0bf791476c58f78b412196ff3eb 3	lib/payments-machine-flow.ts
100644 b1f7ce30be1c23ec7b8f53cd2196d1dff362f077 1	lib/payments-mercadopago.ts
100644 c4d70307467a06cd532c0ca0cd28a20a3bd05c36 2	lib/payments-mercadopago.ts
100644 55b225f4ab2e9a4cffc9e6dafb49306ca55fc770 3	lib/payments-mercadopago.ts


**Inspecting file paths**

I’m trying to understand why there’s no stage2 for machine-flow, which seems odd. I wonder if the app imports are still there because the HEAD has an old route and there's no machine-flow. The current working tree’s file is likely just from stage3.

Since TCN route imports are present, maybe the TCN route is missing from the main branch? I’ll search for app/api/integrations/tcn, and it seems like the route path exists in the working tree. Let's check with git ls-files next.

git ls-files app/api/integrations/tcn/payments/init/route.ts lib/payments-machine-flow.ts


lib/payments-machine-flow.ts
lib/payments-machine-flow.ts


git cherry-pick --abort && git status --short --branch


## main...origin/main [behind 15]


  return {
    ok: true,
    value: {
      sessionId,
      vendingMachineId: correlation.parsedReference.vendingMachineId,
      productId,
      machineTxnId: correlation.parsedReference.machineTxnId,
      externalReference,
      amountMinor,
      currency: order.currency?.trim() || "ARS",
      status: "approved",
      approvedAt,
      lateApproval: false,
      providerPaymentId,
    },
  };
}

export async function createMercadoPagoQrOrder(input: {
  vendingMachineId: string;
  productId: string;
  amountMinor: number;
  currency: string;
  externalReference: string;
  machineTxnId: string;
  externalPosId: string;
  mode: "static" | "dynamic" | "hybrid";
  description: string;
  expirationTime: string;
  notificationUrl?: string;
}) {
  const config = getMercadoPagoConfig();

  if (!config.accessToken) {
    return {
      ok: false as const,
      status: 500,
      error: "MERCADOPAGO_ACCESS_TOKEN_NOT_CONFIGURED",
    };
  }

  const response = await fetch(`${config.publicApiBaseUrl}/v1/orders`, {
    method: "POST",
    cache: "no-store",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.accessToken}`,
      "X-Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({
      type: "qr",
      total_amount: amountMinorToMercadoPagoAmount(input.amountMinor),
      description: input.description,
      external_reference: input.externalReference,
      expiration_time: input.expirationTime,
      config: {
        qr: {
          external_pos_id: input.externalPosId,
          mode: input.mode,
        },
      },
      transactions: {
        payments: [
          {
            amount: amountMinorToMercadoPagoAmount(input.amountMinor),
          },
        ],
      },
      items: [
        {
          title: input.description,
          unit_price: amountMinorToMercadoPagoAmount(input.amountMinor),
          quantity: 1,
          unit_measure: "unit",
          external_code: input.productId,
          external_categories: [{ id: "ballbox-product" }],
        },
      ],
      ...(input.notificationUrl
        ? {
            notification_url: input.notificationUrl,
          }
        : {}),
    }),
  });

  const payload = (await response.json().catch(() => null)) as
    | {
        id?: string | number;
        external_reference?: string;
        status?: string;
        status_detail?: string;
        expiration_time?: string;
        type_response?: { qr_data?: string };
      }
    | null;

  if (!response.ok) {
    return {
      ok: false as const,
      status: response.status,
      error: `MERCADOPAGO_ORDER_CREATE_FAILED_${response.status}`,
      details: payload,
    };
  }

  return {
    ok: true as const,
    value: {
      orderId: payload?.id ? String(payload.id) : "",
      externalReference: payload?.external_reference ?? input.externalReference,
      vendingMachineId: input.vendingMachineId,
      productId: input.productId,
      machineTxnId: input.machineTxnId,
      amountMinor: input.amountMinor,
      currency: input.currency,
      mode: input.mode,
      externalPosId: input.externalPosId,

[8 more lines in file. Use offset=300 to continue.]


- TVs errors now use a shared strict shape and the contract is validated by `pnpm test:contracts`.
- Payments integration health and sessions routes now report Ballbox-internal native MercadoPago status plus webhook/order metadata.
- Payments native MercadoPago webhook flow now lands in Ballbox, validates canonical payment correlation from fetched orders, and persists approved payments as `PaymentSession` records.
- QR order creation now sets a public `notification_url` so Mercado Pago routes order updates back to Ballbox explicitly.
- Unsupported merchant-order notification topics are ignored so only QR order events drive persistence.
- Replay testing with two older live `ORD...` ids returned `MERCADOPAGO_ORDER_NOT_APPROVED_expired`; see `docs/mercadopago-webhook-replay-notes-2026-04-23.md` for the current blocker and next resume point.
- Payments now have an admin CRUD/reconciliation surface over local `PaymentSession` rows via `/api/admin/payments/*`.
- Final target model is documented in `docs/domain-model-target.md`; end state does not keep retro-compatibility shims.
- ESLint is now wired and the repo exposes an explicit `typecheck` script.
- `/admin` and current admin-only APIs can now be gated by a shared password cookie session.
- `/admin/tvs` now performs admin CRUD for screens and tv devices through admin-only APIs under `/api/admin/tvs/*`.
- `/admin/tvs` now performs admin CRUD for campaigns, creatives, and assignments through admin-only APIs under `/api/admin/tvs/*`.
- TVs impression writes now validate creative-to-screen activity windows before persisting telemetry.
- Players now have DB-backed read/admin APIs for `Player` and `Membership`.
- Players route + page callers now import `lib/players-backend.ts` (and `lib/network-backend-clubs.ts` for club lists) directly instead of relying on `lib/ballbox-store.ts` for runtime access.
- Sports now have DB-backed read/admin APIs for `Coach`, `OpenMatch`, and `Class`.
- Classes MVP now also has DB-backed public APIs for `ClassOffer` and `ClassRequest`.
- `/classes/me` now provides a lightweight student request-history surface over Ballbox-local `ClassRequest` records (read-only, demo-grade email lookup).
- `ClassRequest` now snapshots the student's selected visible slot when they choose one, and otherwise persists an explicit flexible-selection fallback mode.
- Coach availability now ships as weekly ranges with 90-minute slots, filtered by coach club selection and ATC club availability.
- Coach and club account entry now starts at `/coaches`, with `Sign in` first and `Sign up` only for users without an account.
- Coaches and clubs now sign in with per-account email + password credentials, and `/api/account/session` routes each session to the correct dashboard.
- Coach sign-up now asks only for name, email, and password; if a club invited that coach, the invite link auto-links the account to that club.
- `/coaches/settings` now opens on a dashboard split into `Calendar` and `Analytics`, then lets coaches move into configuration from the shared navbar to choose external clubs, edit weekly availability only inside allowed venue/club windows, configure reusable class packages, and save a Mercado Pago placeholder handle.
- `/coaches/settings` now also lets coaches attach existing Ballbox players to concrete booked slots in their active venues, so taken hours, weekly earnings, consistency tracking, and lightweight student follow-up live inside the coach workspace.
- Club sign-in/sign-up now lands on `/clubs`, a service home that separates the club's operational modules before entering the detailed coaches workspace.
- Club accounts now have a dedicated workspace at `/clubs/coaches` split into `Calendar`, `Analytics`, `Sedes`, and `Profesores`, while venue-by-venue deep setup still lives at `/clubs/venues/[venueId]`, all under the same shared navbar/profile chrome.
- Venue setup now uses two simple fields for opening and closing time plus a collapsible weekly half-hour grid for coach hours, shows closed periods in red while editing coach availability, lets clubs mark whether external coaches are allowed, keeps coach-enabled courts in a dedicated selection panel, and includes reservation pricing/cancellation settings both in the venue detail and as quick settings on `/clubs/coaches`, including a discount that applies only to coaches assigned to that club.
- ATC lookups for class surfaces now resolve by club (no court coupling), and class request validation rechecks the selected slot date/time against ATC public-read.
- `/classes` SSR now avoids blocking first paint on ATC public-read fetches; the page paints local offer data first, then hydrates ATC availability from cached API responses.
- `/admin` overview SSR now uses direct aggregate counts instead of loading full admin datasets only to derive metrics.
- Sports/classes facade exports in `lib/ballbox-store.ts` now delegate to dedicated sports-events and classes backend modules instead of keeping that slice inline.
- Sports route + page callers now import `lib/sports-backend-events.ts` and `lib/sports-backend-coaches.ts` directly, so `lib/ballbox-store.ts` is no longer the primary sports entrypoint.
- TV runtime/backend facade exports in `lib/ballbox-store.ts` now delegate to `lib/tvs-backend-runtime.ts` instead of keeping source/manifest/heartbeat/impression logic inline.
- UI color/system tokens are split between landing (`app/styles/landing-tokens.css`) and admin (`app/styles/admin-tokens.css`).

## Streetcast Migration Check
- Source of truth for parity review: `/Users/sebas/Code/street-cast-pwa`.
- Covered in Ballbox today:
  - manifest polling exists in `components/screen-player.tsx`
  - sequential creative playback exists in `components/screen-player.tsx`
  - impression writes exist in `components/screen-player.tsx`
  - runtime health signaling exists via heartbeats in `components/screen-player.tsx`
- Missing or not yet verified against Streetcast behavior:
  - no manual real-device verification yet proves CacheStorage/object-URL playback outside local browser automation

## Next Steps (Priority + Dependency Queue)
- Source of truth for task backlog and dependencies: `docs/next-steps-queue.md`.
- Selection rule for the next agent: pick the highest-priority task marked `ready` in that doc.
- Current pick (`P0`, `ready`): `OPS-1` payments real-world validation closure.
- Immediately after `OPS-1`: `OPS-2` TVs runtime parity closure.
- Then continue with `P1` in dependency order: `ARCH-2` and `ARCH-3` -> `PERF-1` -> `PAY-3`.
- Club revenue share is currently handled with scheduled manual settlements; `PAY-4` stays behind the TCN machine-facing adapter work as a future Mercado Pago split option.
- Keep detailed backlog/status only in `docs/next-steps-queue.md` to avoid drift.

## Ordered Payments Track
1. Finish provider-level Mercado Pago proof without TCN hardware.
   - backlog: `OPS-1` in `docs/next-steps-queue.md`
   - docs: `docs/implemented-payments.md`, `docs/mercadopago-webhook-replay-notes-2026-04-23.md`
2. Ask TCN sales to confirm the remaining machine-contract questions with their engineers.
   - docs: `docs/tcn-payments-open-questions.md`, `docs/tcn-payments-integration-plan.md`
   - note: this is required because the machines are not here yet
3. Freeze the Ballbox <-> TCN adapter contract once those answers come back.
   - docs: `docs/tcn-payments-integration-plan.md`, `docs/tcn-payments-execution-plan.md`
4. Implement one machine payment flow with two owner modes.
   - `BALLBOX`: Ballbox-collected club machines
   - `TENANT`: tenant/client-collected machines such as Adidas rentals
   - backlog: `PAY-3` in `docs/next-steps-queue.md`
   - doc: `docs/payments-machine-rollout-rfc.md`
5. Use guided onboarding for the first wave of 5 machines.
   - first wave target: 3 `BALLBOX` machines + 2 `TENANT` machines
   - docs: `docs/payments-machine-rollout-rfc.md`, `docs/tcn-payments-execution-plan.md`
6. Keep club settlements informal/manual for Ballbox-collected machines until the machine path is stable and settlement pain is real.
   - backlog: `PAY-4` in `docs/next-steps-queue.md`
   - doc: `docs/payments-club-settlement-strategy.md`
7. Revisit automation later only if volume or ops pain justify it.
   - current future candidate: Mercado Pago split / marketplace

### Payments Track Docs
- Master rollout RFC: `docs/payments-machine-rollout-rfc.md`
- Technical implementation RFC: `docs/payments-machine-implementation-rfc.md`
- Implemented provider layer: `docs/implemented-payments.md`
- TCN direction and target flow: `docs/tcn-payments-integration-plan.md`
- Open external questions: `docs/tcn-payments-open-questions.md`
- Ordered execution sequence: `docs/tcn-payments-execution-plan.md`
- Club settlement strategy: `docs/payments-club-settlement-strategy.md`
- Current Mercado Pago webhook blocker/evidence: `docs/mercadopago-webhook-replay-notes-2026-04-23.md`
- TCN protocol evidence artifact: `test-results/tcn-payment-protocol/findings-2026-04-23.md`

### Product Continuation Constraints

[87 more lines in file. Use offset=240 to continue.]


## Integration Contract
- Ballbox native MercadoPago webhook path is `/notification/mercadopago`.
- Ballbox native QR order creation path is `/api/integrations/payments/orders`.
- That route now also attaches a public `notification_url` so Mercado Pago sends QR order updates back to `/notification/mercadopago`.
- That route accepts an explicit amount for arbitrary charges or falls back to the Ballbox `Product.priceMinor` when the amount is omitted, which powers per-product QR generation in the admin UI.
- The order route rejects values below Mercado Pago's minimum charge (`15.00 ARS`) before calling the provider.
- QR webhook persistence now depends on the upstream order fetch plus canonical-reference mapping; merchant-order topic events are ignored.
- Legacy approved-callback path `/api/integrations/payments/approved` is removed as an operational integration and now returns `410 PAYMENTS_APPROVED_CALLBACK_REMOVED`.
- Native payment correlation requires canonical `externalReference` values shaped as `payment-vm_{vendingMachineId}-txn_{machineTxnId}`.
- `NEXT_PUBLIC_SITE_URL` is required if Ballbox should expose a concrete `recommendedMercadoPagoWebhookUrl`.

## Validation Status
- Host app now owns the native MercadoPago order creation + webhook persistence path inside Ballbox.
- Remaining gap is operational/manual: one real public webhook + one real approved payment persisted and visible in `/admin/payments`.
- Club revenue share is currently handled through scheduled manual settlements/bookkeeping for Ballbox-collected machines; no provider-native split payout path is live.
- First rollout planning now assumes one shared technical flow with two payment-owner modes: `BALLBOX` for club machines Ballbox collects on, and `TENANT` for client-owned collection setups such as Adidas rental machines; see `docs/payments-machine-rollout-rfc.md`.
- The admin surface includes a live QR generator preview for provider-level scan-and-pay validation, but production vending flow should move behind the TCN machine protocol instead of relying on that admin page.
- The webhook route is fetch-first: the webhook is just the trigger, and Ballbox verifies the order state by fetching Mercado Pago before persisting.
- TCN protocol review indicates the `additional hardware devices` warning applies to machine-side QR scanning, while the payment flow (`FunCode=8000`) returns `CodeUrl` for machine-screen QR display; see `docs/tcn-payments-integration-plan.md` and `test-results/tcn-payment-protocol/findings-2026-04-23.md`.
- Recent replay validation against two older live `ORD...` ids returned `MERCADOPAGO_ORDER_NOT_APPROVED_expired`; see `docs/mercadopago-webhook-replay-notes-2026-04-23.md` when resuming webhook persistence checks.

[16 more lines in file. Use offset=50 to continue.]


### OPS-1 Payments real-world validation closure
- Priority: P0
- Status: ready
- Depends on: none
- Scope:
  - run one public webhook smoke against the native MercadoPago webhook path
  - run one real payment and verify persisted approved payment in `/admin/payments`
  - document observed payload + public URL evidence
- Progress (2026-04-14): payments migration to host app started in one feature branch; health/sessions now expose internal MercadoPago mode, Ballbox ships `/notification/mercadopago` intake with order fetch verification, and native order creation now exists at `/api/integrations/payments/orders`.
- Progress (2026-04-19): PAY-2 cleanup removed bridge-oriented operational status from host-app health/docs; OPS-1 is now focused on validating the native MercadoPago order-create + webhook-persist path only.
- Progress (2026-04-23): TCN protocol review shows the `additional hardware devices` note is about machine-side QR scanning, while the machine payment flow (`FunCode=8000`) already returns `CodeUrl` for machine-screen QR display. This means the admin QR page should be treated as a provider test harness, and production integration should move to a TCN adapter layer.
- Expected payoff: closes highest-risk integration uncertainty

### OPS-2 TVs runtime parity closure
- Priority: P0
- Status: ready

[124 more lines in file. Use offset=80 to continue.]


# Mercado Pago webhook replay notes

Date: 2026-04-23

## What was tried
- Replayed real Ballbox Mercado Pago webhook payloads against `https://ballbox.app/notification/mercadopago`.
- Tested these order ids from prior prod logs:
  - `ORD01KPV95KCFED46SQ2J23C8JR75`
  - `ORD01KPV68YK3W6HPH29ZK6ZY58G8`

## Result
- Both replays returned `202 Accepted`.
- Ballbox ignored both because Mercado Pago order fetch returned the orders as expired:
  - `MERCADOPAGO_ORDER_NOT_APPROVED_expired`

## Meaning
- The webhook endpoint is live.
- Order fetch works.
- Persist-only-on-approved logic is working.
- These specific replay ids are no longer useful for persistence validation because the orders expired in Mercado Pago.

## Current blocker
- Need either:
  - a fresh approved `order.processed` id still valid in Mercado Pago, or
  - a real buyer-side payment to create a new approved order.

## Recommended next step
- Keep the webhook path as-is.
- When resuming, use a fresh approved order id from a recent live payment or the buyer app flow.


**Planning changes manually**

I need to apply our changes manually on the main, plus consider any additions based on the user's request to leave it written in the plan. So, I'll patch the relevant documentation alongside the code. I might also want to remove the import of `getPublicAppUrl` from the route. Let's get started on making these edits now! It's a good time to ensure everything stays organized and clear for the user.

edit

export async function createMercadoPagoQrOrder(input: {
  vendingMachineId: string;
  productId: string;
  amountMinor: number;
  currency: string;
  externalReference: string;
  machineTxnId: string;
  externalPosId: string;
  mode: "static" | "dynamic" | "hybrid";
  description: string;
  expirationTime: string;
  notificationUrl?: string;
}) {
  const config = getMercadoPagoConfig();

export async function createMercadoPagoQrOrder(input: {
  vendingMachineId: string;
  productId: string;
  amountMinor: number;
  currency: string;
  externalReference: string;
  machineTxnId: string;
  externalPosId: string;
  mode: "static" | "dynamic" | "hybrid";
  description: string;
  expirationTime: string;
}) {
  const config = getMercadoPagoConfig();

      items: [
        {
          title: input.description,
          unit_price: amountMinorToMercadoPagoAmount(input.amountMinor),
          quantity: 1,
          unit_measure: "unit",
          external_code: input.productId,
          external_categories: [{ id: "ballbox-product" }],
        },
      ],
      ...(input.notificationUrl
        ? {
            notification_url: input.notificationUrl,
          }
        : {}),
    }),
  });

      items: [
        {
          title: input.description,
          unit_price: amountMinorToMercadoPagoAmount(input.amountMinor),
          quantity: 1,
          unit_measure: "unit",
          external_code: input.productId,
          external_categories: [{ id: "ballbox-product" }],
        },
      ],
    }),
  });


Successfully replaced 2 block(s) in lib/payments-mercadopago.ts.

     ...
 206   externalPosId: string;
 207   mode: "static" | "dynamic" | "hybrid";
 208   description: string;
 209   expirationTime: string;
-210   notificationUrl?: string;
 211 }) {
 212   const config = getMercadoPagoConfig();
 213
 214   if (!config.accessToken) {
     ...
 256           external_code: input.productId,
 257           external_categories: [{ id: "ballbox-product" }],
 258         },
 259       ],
-260       ...(input.notificationUrl
-261         ? {
-262             notification_url: input.notificationUrl,
-263           }
-264         : {}),
 265     }),
 266   });
 267
 268   const payload = (await response.json().catch(() => null)) as
     ...


import { createMercadoPagoQrOrder, getMercadoPagoConfig } from "@/lib/payments-mercadopago";
import { buildPaymentExternalReference, getPaymentProductDefaultPriceMinor } from "@/lib/payments";
import { getPublicAppUrl } from "@/lib/public-url";
import { prisma } from "@/lib/prisma";

import { createMercadoPagoQrOrder, getMercadoPagoConfig } from "@/lib/payments-mercadopago";
import { buildPaymentExternalReference, getPaymentProductDefaultPriceMinor } from "@/lib/payments";
import { prisma } from "@/lib/prisma";

  const description =
    parsedBody.data.description?.trim() || `${product.name} (${vendingMachine.label || vendingMachine.id})`;
  const publicAppUrl = getPublicAppUrl();
  const notificationUrl = publicAppUrl ? `${publicAppUrl}/notification/mercadopago?source_news=webhooks` : undefined;

  const createResult = await createMercadoPagoQrOrder({

  const description =
    parsedBody.data.description?.trim() || `${product.name} (${vendingMachine.label || vendingMachine.id})`;

  const createResult = await createMercadoPagoQrOrder({

    externalPosId,
    mode: parsedBody.data.mode,
    description,
    expirationTime: parsedBody.data.expirationTime,
    notificationUrl,
  });

    externalPosId,
    mode: parsedBody.data.mode,
    description,
    expirationTime: parsedBody.data.expirationTime,
  });


Successfully replaced 3 block(s) in app/api/integrations/payments/orders/route.ts.

     ...
  10   paymentsJsonResponse,
  11 } from "@/lib/http/payments-route-helpers";
  12 import { createMercadoPagoQrOrder, getMercadoPagoConfig } from "@/lib/payments-mercadopago";
  13 import { buildPaymentExternalReference, getPaymentProductDefaultPriceMinor } from "@/lib/payments";
- 14 import { getPublicAppUrl } from "@/lib/public-url";
  15 import { prisma } from "@/lib/prisma";
  16
  17 function buildMachineTxnId() {
  18   return `txn_${Date.now()}_${crypto.randomUUID().slice(0, 8)}`;
     ...
  88   }
  89
  90   const description =
  91     parsedBody.data.description?.trim() || `${product.name} (${vendingMachine.label || vendingMachine.id})`;
- 92   const publicAppUrl = getPublicAppUrl();
- 93   const notificationUrl = publicAppUrl ? `${publicAppUrl}/notification/mercadopago?source_news=webhooks` : undefined;
  94
  95   const createResult = await createMercadoPagoQrOrder({
  96     vendingMachineId: parsedBody.data.vendingMachineId,
  97     productId: parsedBody.data.productId,
     ...
 102     externalPosId,
 103     mode: parsedBody.data.mode,
 104     description,
 105     expirationTime: parsedBody.data.expirationTime,
-106     notificationUrl,
 107   });
 108
 109   if (!createResult.ok) {
 110     return paymentsErrorResponse("PAYMENT_SESSION_CONFLICT", 502, {
     ...


- Payments native MercadoPago webhook flow now lands in Ballbox, validates canonical payment correlation from fetched orders, and persists approved payments as `PaymentSession` records.
- QR order creation now sets a public `notification_url` so Mercado Pago routes order updates back to Ballbox explicitly.

- Payments native MercadoPago webhook flow now lands in Ballbox, validates canonical payment correlation from fetched orders, and persists approved payments as `PaymentSession` records.
- For the current Ballbox Mercado Pago account/API surface, QR order creation now relies on the account-level webhook configuration instead of sending inline `notification_url`, because Mercado Pago rejects that field on `POST /v1/orders` for this setup.

1. Finish provider-level Mercado Pago proof without TCN hardware.
   - backlog: `OPS-1` in `docs/next-steps-queue.md`
   - docs: `docs/implemented-payments.md`, `docs/mercadopago-webhook-replay-notes-2026-04-23.md`
2. Ask TCN sales to confirm the remaining machine-contract questions with their engineers.

1. Finish provider-level Mercado Pago proof without TCN hardware.
   - backlog: `OPS-1` in `docs/next-steps-queue.md`
   - done condition: one real production order created by Ballbox, one real approved Mercado Pago payment, public webhook delivery to `/notification/mercadopago`, Ballbox fetch-and-persist of the approved order, and visible reconciliation evidence in `/admin/payments`
   - docs: `docs/implemented-payments.md`, `docs/mercadopago-webhook-replay-notes-2026-04-23.md`
2. After `OPS-1`, treat provider risk as materially reduced but not machine risk.
   - still required for machine confidence: real TCN `8000` request validation, `CodeUrl` rendering confirmation, approval-to-dispense handshake proof, duplicate/retry handling, timeout/expiry handling, and an operator runbook for approved-but-no-vend incidents
3. Ask TCN sales to confirm the remaining machine-contract questions with their engineers.

3. Freeze the Ballbox <-> TCN adapter contract once those answers come back.

4. Freeze the Ballbox <-> TCN adapter contract once those answers come back.

4. Implement one machine payment flow with two owner modes.

5. Implement one machine payment flow with two owner modes.

5. Use guided onboarding for the first wave of 5 machines.

6. Use guided onboarding for the first wave of 5 machines.

6. Keep club settlements informal/manual for Ballbox-collected machines until the machine path is stable and settlement pain is real.

7. Keep club settlements informal/manual for Ballbox-collected machines until the machine path is stable and settlement pain is real.

7. Revisit automation later only if volume or ops pain justify it.

8. Revisit automation later only if volume or ops pain justify it.


Successfully replaced 7 block(s) in PLAN.md.

     ...
 149 - TVs admin now exposes direct runtime and manifest links per screen for operations.
 150 - TVs errors now use a shared strict shape and the contract is validated by `pnpm test:contracts`.
 151 - Payments integration health and sessions routes now report Ballbox-internal native MercadoPago status plus webhook/order metadata.
 152 - Payments native MercadoPago webhook flow now lands in Ballbox, validates canonical payment correlation from fetched orders, and persists approved payments as `PaymentSession` records.
-153 - QR order creation now sets a public `notification_url` so Mercado Pago routes order updates back to Ballbox explicitly.
+153 - For the current Ballbox Mercado Pago account/API surface, QR order creation now relies on the account-level webhook configuration instead of sending inline `notification_url`, because Mercado Pago rejects that field on `POST /v1/orders` for this setup.
 154 - Unsupported merchant-order notification topics are ignored so only QR order events drive persistence.
 155 - Replay testing with two older live `ORD...` ids returned `MERCADOPAGO_ORDER_NOT_APPROVED_expired`; see `docs/mercadopago-webhook-replay-notes-2026-04-23.md` for the current blocker and next resume point.
 156 - Payments now have an admin CRUD/reconciliation surface over local `PaymentSession` rows via `/api/admin/payments/*`.
 157 - Final target model is documented in `docs/domain-model-target.md`; end state does not keep retro-compatibility shims.
     ...
 204
 205 ## Ordered Payments Track
 206 1. Finish provider-level Mercado Pago proof without TCN hardware.
 207    - backlog: `OPS-1` in `docs/next-steps-queue.md`
+208    - done condition: one real production order created by Ballbox, one real approved Mercado Pago payment, public webhook delivery to `/notification/mercadopago`, Ballbox fetch-and-persist of the approved order, and visible reconciliation evidence in `/admin/payments`
 208    - docs: `docs/implemented-payments.md`, `docs/mercadopago-webhook-replay-notes-2026-04-23.md`
-209 2. Ask TCN sales to confirm the remaining machine-contract questions with their engineers.
+210 2. After `OPS-1`, treat provider risk as materially reduced but not machine risk.
+211    - still required for machine confidence: real TCN `8000` request validation, `CodeUrl` rendering confirmation, approval-to-dispense handshake proof, duplicate/retry handling, timeout/expiry handling, and an operator runbook for approved-but-no-vend incidents
+212 3. Ask TCN sales to confirm the remaining machine-contract questions with their engineers.
 210    - docs: `docs/tcn-payments-open-questions.md`, `docs/tcn-payments-integration-plan.md`
 211    - note: this is required because the machines are not here yet
-212 3. Freeze the Ballbox <-> TCN adapter contract once those answers come back.
+215 4. Freeze the Ballbox <-> TCN adapter contract once those answers come back.
 213    - docs: `docs/tcn-payments-integration-plan.md`, `docs/tcn-payments-execution-plan.md`
-214 4. Implement one machine payment flow with two owner modes.
+217 5. Implement one machine payment flow with two owner modes.
 215    - `BALLBOX`: Ballbox-collected club machines
 216    - `TENANT`: tenant/client-collected machines such as Adidas rentals
 217    - backlog: `PAY-3` in `docs/next-steps-queue.md`
 218    - doc: `docs/payments-machine-rollout-rfc.md`
-219 5. Use guided onboarding for the first wave of 5 machines.
+222 6. Use guided onboarding for the first wave of 5 machines.
 220    - first wave target: 3 `BALLBOX` machines + 2 `TENANT` machines
 221    - docs: `docs/payments-machine-rollout-rfc.md`, `docs/tcn-payments-execution-plan.md`
-222 6. Keep club settlements informal/manual for Ballbox-collected machines until the machine path is stable and settlement pain is real.
+225 7. Keep club settlements informal/manual for Ballbox-collected machines until the machine path is stable and settlement pain is real.
 223    - backlog: `PAY-4` in `docs/next-steps-queue.md`
 224    - doc: `docs/payments-club-settlement-strategy.md`
-225 7. Revisit automation later only if volume or ops pain justify it.
+228 8. Revisit automation later only if volume or ops pain justify it.
 226    - current future candidate: Mercado Pago split / marketplace
 227
 228 ### Payments Track Docs
 229 - Master rollout RFC: `docs/payments-machine-rollout-rfc.md`
     ...


- That route now also attaches a public `notification_url` so Mercado Pago sends QR order updates back to `/notification/mercadopago`.

- For the current Ballbox Mercado Pago account/API surface, QR order create relies on the account-level webhook configuration instead of sending inline `notification_url` in the order payload, because Mercado Pago rejects that field on `POST /v1/orders` for this setup.


Successfully replaced 1 block(s) in docs/implemented-payments.md.

    ...
 29
 30 ## Integration Contract
 31 - Ballbox native MercadoPago webhook path is `/notification/mercadopago`.
 32 - Ballbox native QR order creation path is `/api/integrations/payments/orders`.
-33 - That route now also attaches a public `notification_url` so Mercado Pago sends QR order updates back to `/notification/mercadopago`.
+33 - For the current Ballbox Mercado Pago account/API surface, QR order create relies on the account-level webhook configuration instead of sending inline `notification_url` in the order payload, because Mercado Pago rejects that field on `POST /v1/orders` for this setup.
 34 - That route accepts an explicit amount for arbitrary charges or falls back to the Ballbox `Product.priceMinor` when the amount is omitted, which powers per-product QR generation in the admin UI.
 35 - The order route rejects values below Mercado Pago's minimum charge (`15.00 ARS`) before calling the provider.
 36 - QR webhook persistence now depends on the upstream order fetch plus canonical-reference mapping; merchant-order topic events are ignored.
 37 - Legacy approved-callback path `/api/integrations/payments/approved` is removed as an operational integration and now returns `410 PAYMENTS_APPROVED_CALLBACK_REMOVED`.
    ...


- Progress (2026-04-23): TCN protocol review shows the `additional hardware devices` note is about machine-side QR scanning, while the machine payment flow (`FunCode=8000`) already returns `CodeUrl` for machine-screen QR display. This means the admin QR page should be treated as a provider test harness, and production integration should move to a TCN adapter layer.
- Expected payoff: closes highest-risk integration uncertainty

- Progress (2026-04-23): TCN protocol review shows the `additional hardware devices` note is about machine-side QR scanning, while the machine payment flow (`FunCode=8000`) already returns `CodeUrl` for machine-screen QR display. This means the admin QR page should be treated as a provider test harness, and production integration should move to a TCN adapter layer.
- Progress (2026-05-05): first production order-create attempt against `https://ballbox.app/api/integrations/payments/orders` failed before buyer payment with Mercado Pago `400 unsupported_properties` because `notification_url` was rejected (`additionalProperties '$.notification_url' not allowed`). Ballbox should stop sending inline `notification_url` for this account/API surface and rely on the existing account-level Mercado Pago webhook configuration before rerunning the real-payment check.
- Expected payoff: closes highest-risk integration uncertainty