1 Commits

Author SHA1 Message Date
Michilis
fc4af38e8a Add per-quantity TPago payment links for multi-ticket checkout.
Each ticket quantity (1-5) can have its own TPago link so checkout, emails, and admin config use the correct fixed-amount URL.
2026-06-18 23:59:17 +00:00
11 changed files with 195 additions and 21 deletions

View File

@@ -259,6 +259,10 @@ async function migrate() {
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
tpago_enabled INTEGER NOT NULL DEFAULT 0, tpago_enabled INTEGER NOT NULL DEFAULT 0,
tpago_link TEXT, tpago_link TEXT,
tpago_link_2 TEXT,
tpago_link_3 TEXT,
tpago_link_4 TEXT,
tpago_link_5 TEXT,
tpago_instructions TEXT, tpago_instructions TEXT,
tpago_instructions_es TEXT, tpago_instructions_es TEXT,
bank_transfer_enabled INTEGER NOT NULL DEFAULT 0, bank_transfer_enabled INTEGER NOT NULL DEFAULT 0,
@@ -283,6 +287,13 @@ async function migrate() {
await (db as any).run(sql`ALTER TABLE payment_options ADD COLUMN allow_duplicate_bookings INTEGER NOT NULL DEFAULT 0`); await (db as any).run(sql`ALTER TABLE payment_options ADD COLUMN allow_duplicate_bookings INTEGER NOT NULL DEFAULT 0`);
} catch (e) { /* column may already exist */ } } catch (e) { /* column may already exist */ }
// Add per-quantity TPago link columns to payment_options if they don't exist
for (const col of ['tpago_link_2', 'tpago_link_3', 'tpago_link_4', 'tpago_link_5']) {
try {
await (db as any).run(sql.raw(`ALTER TABLE payment_options ADD COLUMN ${col} TEXT`));
} catch (e) { /* column may already exist */ }
}
// Event payment overrides table // Event payment overrides table
await (db as any).run(sql` await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS event_payment_overrides ( CREATE TABLE IF NOT EXISTS event_payment_overrides (
@@ -290,6 +301,10 @@ async function migrate() {
event_id TEXT NOT NULL REFERENCES events(id), event_id TEXT NOT NULL REFERENCES events(id),
tpago_enabled INTEGER, tpago_enabled INTEGER,
tpago_link TEXT, tpago_link TEXT,
tpago_link_2 TEXT,
tpago_link_3 TEXT,
tpago_link_4 TEXT,
tpago_link_5 TEXT,
tpago_instructions TEXT, tpago_instructions TEXT,
tpago_instructions_es TEXT, tpago_instructions_es TEXT,
bank_transfer_enabled INTEGER, bank_transfer_enabled INTEGER,
@@ -309,6 +324,13 @@ async function migrate() {
) )
`); `);
// Add per-quantity TPago link columns to event_payment_overrides if they don't exist
for (const col of ['tpago_link_2', 'tpago_link_3', 'tpago_link_4', 'tpago_link_5']) {
try {
await (db as any).run(sql.raw(`ALTER TABLE event_payment_overrides ADD COLUMN ${col} TEXT`));
} catch (e) { /* column may already exist */ }
}
await (db as any).run(sql` await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS contacts ( CREATE TABLE IF NOT EXISTS contacts (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@@ -702,6 +724,10 @@ async function migrate() {
id UUID PRIMARY KEY, id UUID PRIMARY KEY,
tpago_enabled INTEGER NOT NULL DEFAULT 0, tpago_enabled INTEGER NOT NULL DEFAULT 0,
tpago_link VARCHAR(500), tpago_link VARCHAR(500),
tpago_link_2 VARCHAR(500),
tpago_link_3 VARCHAR(500),
tpago_link_4 VARCHAR(500),
tpago_link_5 VARCHAR(500),
tpago_instructions TEXT, tpago_instructions TEXT,
tpago_instructions_es TEXT, tpago_instructions_es TEXT,
bank_transfer_enabled INTEGER NOT NULL DEFAULT 0, bank_transfer_enabled INTEGER NOT NULL DEFAULT 0,
@@ -726,12 +752,23 @@ async function migrate() {
await (db as any).execute(sql`ALTER TABLE payment_options ADD COLUMN allow_duplicate_bookings INTEGER NOT NULL DEFAULT 0`); await (db as any).execute(sql`ALTER TABLE payment_options ADD COLUMN allow_duplicate_bookings INTEGER NOT NULL DEFAULT 0`);
} catch (e) { /* column may already exist */ } } catch (e) { /* column may already exist */ }
// Add per-quantity TPago link columns to payment_options if they don't exist
for (const col of ['tpago_link_2', 'tpago_link_3', 'tpago_link_4', 'tpago_link_5']) {
try {
await (db as any).execute(sql.raw(`ALTER TABLE payment_options ADD COLUMN ${col} VARCHAR(500)`));
} catch (e) { /* column may already exist */ }
}
await (db as any).execute(sql` await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS event_payment_overrides ( CREATE TABLE IF NOT EXISTS event_payment_overrides (
id UUID PRIMARY KEY, id UUID PRIMARY KEY,
event_id UUID NOT NULL REFERENCES events(id), event_id UUID NOT NULL REFERENCES events(id),
tpago_enabled INTEGER, tpago_enabled INTEGER,
tpago_link VARCHAR(500), tpago_link VARCHAR(500),
tpago_link_2 VARCHAR(500),
tpago_link_3 VARCHAR(500),
tpago_link_4 VARCHAR(500),
tpago_link_5 VARCHAR(500),
tpago_instructions TEXT, tpago_instructions TEXT,
tpago_instructions_es TEXT, tpago_instructions_es TEXT,
bank_transfer_enabled INTEGER, bank_transfer_enabled INTEGER,
@@ -751,6 +788,13 @@ async function migrate() {
) )
`); `);
// Add per-quantity TPago link columns to event_payment_overrides if they don't exist
for (const col of ['tpago_link_2', 'tpago_link_3', 'tpago_link_4', 'tpago_link_5']) {
try {
await (db as any).execute(sql.raw(`ALTER TABLE event_payment_overrides ADD COLUMN ${col} VARCHAR(500)`));
} catch (e) { /* column may already exist */ }
}
await (db as any).execute(sql` await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS contacts ( CREATE TABLE IF NOT EXISTS contacts (
id UUID PRIMARY KEY, id UUID PRIMARY KEY,

View File

@@ -135,6 +135,10 @@ export const sqlitePaymentOptions = sqliteTable('payment_options', {
// TPago configuration // TPago configuration
tpagoEnabled: integer('tpago_enabled', { mode: 'boolean' }).notNull().default(false), tpagoEnabled: integer('tpago_enabled', { mode: 'boolean' }).notNull().default(false),
tpagoLink: text('tpago_link'), tpagoLink: text('tpago_link'),
tpagoLink2: text('tpago_link_2'),
tpagoLink3: text('tpago_link_3'),
tpagoLink4: text('tpago_link_4'),
tpagoLink5: text('tpago_link_5'),
tpagoInstructions: text('tpago_instructions'), tpagoInstructions: text('tpago_instructions'),
tpagoInstructionsEs: text('tpago_instructions_es'), tpagoInstructionsEs: text('tpago_instructions_es'),
// Bank Transfer configuration // Bank Transfer configuration
@@ -166,6 +170,10 @@ export const sqliteEventPaymentOverrides = sqliteTable('event_payment_overrides'
// Override flags (null means use global) // Override flags (null means use global)
tpagoEnabled: integer('tpago_enabled', { mode: 'boolean' }), tpagoEnabled: integer('tpago_enabled', { mode: 'boolean' }),
tpagoLink: text('tpago_link'), tpagoLink: text('tpago_link'),
tpagoLink2: text('tpago_link_2'),
tpagoLink3: text('tpago_link_3'),
tpagoLink4: text('tpago_link_4'),
tpagoLink5: text('tpago_link_5'),
tpagoInstructions: text('tpago_instructions'), tpagoInstructions: text('tpago_instructions'),
tpagoInstructionsEs: text('tpago_instructions_es'), tpagoInstructionsEs: text('tpago_instructions_es'),
bankTransferEnabled: integer('bank_transfer_enabled', { mode: 'boolean' }), bankTransferEnabled: integer('bank_transfer_enabled', { mode: 'boolean' }),
@@ -467,6 +475,10 @@ export const pgPaymentOptions = pgTable('payment_options', {
id: uuid('id').primaryKey(), id: uuid('id').primaryKey(),
tpagoEnabled: pgInteger('tpago_enabled').notNull().default(0), tpagoEnabled: pgInteger('tpago_enabled').notNull().default(0),
tpagoLink: varchar('tpago_link', { length: 500 }), tpagoLink: varchar('tpago_link', { length: 500 }),
tpagoLink2: varchar('tpago_link_2', { length: 500 }),
tpagoLink3: varchar('tpago_link_3', { length: 500 }),
tpagoLink4: varchar('tpago_link_4', { length: 500 }),
tpagoLink5: varchar('tpago_link_5', { length: 500 }),
tpagoInstructions: pgText('tpago_instructions'), tpagoInstructions: pgText('tpago_instructions'),
tpagoInstructionsEs: pgText('tpago_instructions_es'), tpagoInstructionsEs: pgText('tpago_instructions_es'),
bankTransferEnabled: pgInteger('bank_transfer_enabled').notNull().default(0), bankTransferEnabled: pgInteger('bank_transfer_enabled').notNull().default(0),
@@ -492,6 +504,10 @@ export const pgEventPaymentOverrides = pgTable('event_payment_overrides', {
eventId: uuid('event_id').notNull().references(() => pgEvents.id), eventId: uuid('event_id').notNull().references(() => pgEvents.id),
tpagoEnabled: pgInteger('tpago_enabled'), tpagoEnabled: pgInteger('tpago_enabled'),
tpagoLink: varchar('tpago_link', { length: 500 }), tpagoLink: varchar('tpago_link', { length: 500 }),
tpagoLink2: varchar('tpago_link_2', { length: 500 }),
tpagoLink3: varchar('tpago_link_3', { length: 500 }),
tpagoLink4: varchar('tpago_link_4', { length: 500 }),
tpagoLink5: varchar('tpago_link_5', { length: 500 }),
tpagoInstructions: pgText('tpago_instructions'), tpagoInstructions: pgText('tpago_instructions'),
tpagoInstructionsEs: pgText('tpago_instructions_es'), tpagoInstructionsEs: pgText('tpago_instructions_es'),
bankTransferEnabled: pgInteger('bank_transfer_enabled'), bankTransferEnabled: pgInteger('bank_transfer_enabled'),

View File

@@ -748,6 +748,10 @@ export const emailService = {
const defaults = { const defaults = {
tpagoEnabled: false, tpagoEnabled: false,
tpagoLink: null, tpagoLink: null,
tpagoLink2: null,
tpagoLink3: null,
tpagoLink4: null,
tpagoLink5: null,
tpagoInstructions: null, tpagoInstructions: null,
tpagoInstructionsEs: null, tpagoInstructionsEs: null,
bankTransferEnabled: false, bankTransferEnabled: false,
@@ -766,6 +770,10 @@ export const emailService = {
return { return {
tpagoEnabled: overrides?.tpagoEnabled ?? global.tpagoEnabled, tpagoEnabled: overrides?.tpagoEnabled ?? global.tpagoEnabled,
tpagoLink: overrides?.tpagoLink ?? global.tpagoLink, tpagoLink: overrides?.tpagoLink ?? global.tpagoLink,
tpagoLink2: overrides?.tpagoLink2 ?? global.tpagoLink2,
tpagoLink3: overrides?.tpagoLink3 ?? global.tpagoLink3,
tpagoLink4: overrides?.tpagoLink4 ?? global.tpagoLink4,
tpagoLink5: overrides?.tpagoLink5 ?? global.tpagoLink5,
tpagoInstructions: overrides?.tpagoInstructions ?? global.tpagoInstructions, tpagoInstructions: overrides?.tpagoInstructions ?? global.tpagoInstructions,
tpagoInstructionsEs: overrides?.tpagoInstructionsEs ?? global.tpagoInstructionsEs, tpagoInstructionsEs: overrides?.tpagoInstructionsEs ?? global.tpagoInstructionsEs,
bankTransferEnabled: overrides?.bankTransferEnabled ?? global.bankTransferEnabled, bankTransferEnabled: overrides?.bankTransferEnabled ?? global.bankTransferEnabled,
@@ -885,7 +893,9 @@ export const emailService = {
// Add payment-method specific variables // Add payment-method specific variables
if (payment.provider === 'tpago') { if (payment.provider === 'tpago') {
variables.tpagoLink = paymentConfig.tpagoLink || ''; // Select the TPago link matching the number of tickets (1-5), falling back to the base link
const tpagoLinkKey = ticketCount <= 1 ? 'tpagoLink' : `tpagoLink${Math.min(ticketCount, 5)}`;
variables.tpagoLink = paymentConfig[tpagoLinkKey] || paymentConfig.tpagoLink || '';
} else { } else {
// Bank transfer // Bank transfer
variables.bankName = paymentConfig.bankName || ''; variables.bankName = paymentConfig.bankName || '';

View File

@@ -18,6 +18,10 @@ const booleanOrNumber = z.union([z.boolean(), z.number()]).transform((val) => {
const updatePaymentOptionsSchema = z.object({ const updatePaymentOptionsSchema = z.object({
tpagoEnabled: booleanOrNumber.optional(), tpagoEnabled: booleanOrNumber.optional(),
tpagoLink: z.string().optional().nullable(), tpagoLink: z.string().optional().nullable(),
tpagoLink2: z.string().optional().nullable(),
tpagoLink3: z.string().optional().nullable(),
tpagoLink4: z.string().optional().nullable(),
tpagoLink5: z.string().optional().nullable(),
tpagoInstructions: z.string().optional().nullable(), tpagoInstructions: z.string().optional().nullable(),
tpagoInstructionsEs: z.string().optional().nullable(), tpagoInstructionsEs: z.string().optional().nullable(),
bankTransferEnabled: booleanOrNumber.optional(), bankTransferEnabled: booleanOrNumber.optional(),
@@ -40,6 +44,10 @@ const updatePaymentOptionsSchema = z.object({
const updateEventOverridesSchema = z.object({ const updateEventOverridesSchema = z.object({
tpagoEnabled: booleanOrNumber.optional().nullable(), tpagoEnabled: booleanOrNumber.optional().nullable(),
tpagoLink: z.string().optional().nullable(), tpagoLink: z.string().optional().nullable(),
tpagoLink2: z.string().optional().nullable(),
tpagoLink3: z.string().optional().nullable(),
tpagoLink4: z.string().optional().nullable(),
tpagoLink5: z.string().optional().nullable(),
tpagoInstructions: z.string().optional().nullable(), tpagoInstructions: z.string().optional().nullable(),
tpagoInstructionsEs: z.string().optional().nullable(), tpagoInstructionsEs: z.string().optional().nullable(),
bankTransferEnabled: booleanOrNumber.optional().nullable(), bankTransferEnabled: booleanOrNumber.optional().nullable(),
@@ -68,6 +76,10 @@ paymentOptionsRouter.get('/', requireAuth(['admin']), async (c) => {
paymentOptions: { paymentOptions: {
tpagoEnabled: false, tpagoEnabled: false,
tpagoLink: null, tpagoLink: null,
tpagoLink2: null,
tpagoLink3: null,
tpagoLink4: null,
tpagoLink5: null,
tpagoInstructions: null, tpagoInstructions: null,
tpagoInstructionsEs: null, tpagoInstructionsEs: null,
bankTransferEnabled: false, bankTransferEnabled: false,
@@ -171,6 +183,10 @@ paymentOptionsRouter.get('/event/:eventId', async (c) => {
const defaults = { const defaults = {
tpagoEnabled: false, tpagoEnabled: false,
tpagoLink: null, tpagoLink: null,
tpagoLink2: null,
tpagoLink3: null,
tpagoLink4: null,
tpagoLink5: null,
tpagoInstructions: null, tpagoInstructions: null,
tpagoInstructionsEs: null, tpagoInstructionsEs: null,
bankTransferEnabled: false, bankTransferEnabled: false,
@@ -193,6 +209,10 @@ paymentOptionsRouter.get('/event/:eventId', async (c) => {
const merged = { const merged = {
tpagoEnabled: overrides?.tpagoEnabled ?? global.tpagoEnabled, tpagoEnabled: overrides?.tpagoEnabled ?? global.tpagoEnabled,
tpagoLink: overrides?.tpagoLink ?? global.tpagoLink, tpagoLink: overrides?.tpagoLink ?? global.tpagoLink,
tpagoLink2: overrides?.tpagoLink2 ?? global.tpagoLink2,
tpagoLink3: overrides?.tpagoLink3 ?? global.tpagoLink3,
tpagoLink4: overrides?.tpagoLink4 ?? global.tpagoLink4,
tpagoLink5: overrides?.tpagoLink5 ?? global.tpagoLink5,
tpagoInstructions: overrides?.tpagoInstructions ?? global.tpagoInstructions, tpagoInstructions: overrides?.tpagoInstructions ?? global.tpagoInstructions,
tpagoInstructionsEs: overrides?.tpagoInstructionsEs ?? global.tpagoInstructionsEs, tpagoInstructionsEs: overrides?.tpagoInstructionsEs ?? global.tpagoInstructionsEs,
bankTransferEnabled: overrides?.bankTransferEnabled ?? global.bankTransferEnabled, bankTransferEnabled: overrides?.bankTransferEnabled ?? global.bankTransferEnabled,

View File

@@ -631,11 +631,21 @@ ticketsRouter.get('/:id', async (c) => {
(db as any).select().from(payments).where(eq((payments as any).ticketId, id)) (db as any).select().from(payments).where(eq((payments as any).ticketId, id))
); );
// Count how many tickets belong to this booking (for per-quantity payment links)
let bookingTicketCount = 1;
if (ticket.bookingId) {
const bookingTickets = await dbAll<any>(
(db as any).select().from(tickets).where(eq((tickets as any).bookingId, ticket.bookingId))
);
bookingTicketCount = bookingTickets.length || 1;
}
return c.json({ return c.json({
ticket: { ticket: {
...ticket, ...ticket,
event, event,
payment, payment,
bookingTicketCount,
}, },
}); });
}); });

View File

@@ -6,7 +6,7 @@ import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext'; import { useLanguage } from '@/context/LanguageContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { eventsApi, ticketsApi, paymentOptionsApi, Event, PaymentOptionsConfig } from '@/lib/api'; import { eventsApi, ticketsApi, paymentOptionsApi, Event, PaymentOptionsConfig } from '@/lib/api';
import { formatPrice, formatDateLong, formatTime } from '@/lib/utils'; import { formatPrice, formatDateLong, formatTime, getTpagoLink } from '@/lib/utils';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
@@ -665,6 +665,7 @@ export default function BookingPage() {
const isTpago = bookingResult.paymentMethod === 'tpago'; const isTpago = bookingResult.paymentMethod === 'tpago';
const ticketCount = bookingResult.ticketCount || 1; const ticketCount = bookingResult.ticketCount || 1;
const totalAmount = (event?.price || 0) * ticketCount; const totalAmount = (event?.price || 0) * ticketCount;
const tpagoLink = getTpagoLink(paymentConfig, ticketCount);
return ( return (
<div className="section-padding"> <div className="section-padding">
@@ -755,9 +756,9 @@ export default function BookingPage() {
<h3 className="font-semibold text-gray-900"> <h3 className="font-semibold text-gray-900">
{locale === 'es' ? 'Pago con Tarjeta' : 'Card Payment'} {locale === 'es' ? 'Pago con Tarjeta' : 'Card Payment'}
</h3> </h3>
{paymentConfig.tpagoLink && ( {tpagoLink && (
<a <a
href={paymentConfig.tpagoLink} href={tpagoLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center justify-center gap-2 w-full px-6 py-4 bg-blue-600 text-white rounded-btn hover:bg-blue-700 transition-colors font-medium" className="flex items-center justify-center gap-2 w-full px-6 py-4 bg-blue-600 text-white rounded-btn hover:bg-blue-700 transition-colors font-medium"

View File

@@ -5,7 +5,7 @@ import { useParams, useSearchParams } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext'; import { useLanguage } from '@/context/LanguageContext';
import { ticketsApi, paymentOptionsApi, Ticket, PaymentOptionsConfig } from '@/lib/api'; import { ticketsApi, paymentOptionsApi, Ticket, PaymentOptionsConfig } from '@/lib/api';
import { formatPrice, formatDateLong, formatTime } from '@/lib/utils'; import { formatPrice, formatDateLong, formatTime, getTpagoLink } from '@/lib/utils';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import { import {
@@ -305,6 +305,7 @@ export default function BookingPaymentPage() {
if (step === 'manual_payment' && ticket && paymentConfig) { if (step === 'manual_payment' && ticket && paymentConfig) {
const isBankTransfer = ticket.payment?.provider === 'bank_transfer'; const isBankTransfer = ticket.payment?.provider === 'bank_transfer';
const isTpago = ticket.payment?.provider === 'tpago'; const isTpago = ticket.payment?.provider === 'tpago';
const tpagoLink = getTpagoLink(paymentConfig, ticket.bookingTicketCount || 1);
return ( return (
<div className="section-padding"> <div className="section-padding">
@@ -418,9 +419,9 @@ export default function BookingPaymentPage() {
<h3 className="font-semibold text-gray-900"> <h3 className="font-semibold text-gray-900">
{locale === 'es' ? 'Pago con Tarjeta' : 'Card Payment'} {locale === 'es' ? 'Pago con Tarjeta' : 'Card Payment'}
</h3> </h3>
{paymentConfig.tpagoLink && ( {tpagoLink && (
<a <a
href={paymentConfig.tpagoLink} href={tpagoLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center justify-center gap-2 w-full px-6 py-4 bg-blue-600 text-white rounded-btn hover:bg-blue-700 transition-colors font-medium" className="flex items-center justify-center gap-2 w-full px-6 py-4 bg-blue-600 text-white rounded-btn hover:bg-blue-700 transition-colors font-medium"

View File

@@ -1519,15 +1519,32 @@ export default function AdminEventDetailPage() {
<div className="space-y-3 pt-3 border-t"> <div className="space-y-3 pt-3 border-t">
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">
{locale === 'es' ? 'Enlace de Pago TPago' : 'TPago Payment Link'} {locale === 'es' ? 'Enlaces de Pago TPago (por cantidad de tickets)' : 'TPago Payment Links (per ticket quantity)'}
</label> </label>
<input <p className="text-[10px] text-gray-500 mb-2">
type="url" {locale === 'es'
value={paymentOverrides.tpagoLink ?? ''} ? 'Cada enlace tiene un monto fijo. Un enlace distinto por cantidad de tickets.'
onChange={(e) => updatePaymentOverride('tpagoLink', e.target.value || null)} : 'Each link has a fixed amount. One link per ticket quantity.'}
placeholder={globalPaymentOptions?.tpagoLink || 'https://www.tpago.com.py/links?alias=...'} </p>
className="w-full px-3 py-2 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow" <div className="space-y-2">
/> {([1, 2, 3, 4, 5] as const).map((qty) => {
const key = (qty === 1 ? 'tpagoLink' : `tpagoLink${qty}`) as keyof PaymentOptionsConfig;
return (
<div key={qty} className="flex items-center gap-2">
<span className="text-xs font-medium text-gray-600 w-20 flex-shrink-0">
{qty} {qty === 1 ? 'ticket' : 'tickets'}
</span>
<input
type="url"
value={(paymentOverrides[key] as string | null) ?? ''}
onChange={(e) => updatePaymentOverride(key, (e.target.value || null) as any)}
placeholder={(globalPaymentOptions?.[key] as string | null) || 'https://www.tpago.com.py/links?alias=...'}
className="flex-1 px-3 py-2 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
/>
</div>
);
})}
</div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div> <div>

View File

@@ -26,6 +26,10 @@ export default function PaymentOptionsPage() {
const [options, setOptions] = useState<PaymentOptionsConfig>({ const [options, setOptions] = useState<PaymentOptionsConfig>({
tpagoEnabled: false, tpagoEnabled: false,
tpagoLink: null, tpagoLink: null,
tpagoLink2: null,
tpagoLink3: null,
tpagoLink4: null,
tpagoLink5: null,
tpagoInstructions: null, tpagoInstructions: null,
tpagoInstructionsEs: null, tpagoInstructionsEs: null,
bankTransferEnabled: false, bankTransferEnabled: false,
@@ -140,13 +144,31 @@ export default function PaymentOptionsPage() {
<div className="space-y-4 pt-4 border-t"> <div className="space-y-4 pt-4 border-t">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
{locale === 'es' ? 'Enlace de Pago TPago' : 'TPago Payment Link'} {locale === 'es' ? 'Enlaces de Pago TPago (por cantidad de tickets)' : 'TPago Payment Links (per ticket quantity)'}
</label> </label>
<Input <p className="text-xs text-gray-500 mb-2">
value={options.tpagoLink || ''} {locale === 'es'
onChange={(e) => updateOption('tpagoLink', e.target.value || null)} ? 'Cada enlace tiene un monto fijo. Usá un enlace distinto para cada cantidad de tickets.'
placeholder="https://www.tpago.com.py/links?alias=..." : 'Each link has a fixed amount. Use a different link for each ticket quantity.'}
/> </p>
<div className="space-y-2">
{([1, 2, 3, 4, 5] as const).map((qty) => {
const key = (qty === 1 ? 'tpagoLink' : `tpagoLink${qty}`) as keyof PaymentOptionsConfig;
return (
<div key={qty} className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-600 w-24 flex-shrink-0">
{qty} {locale === 'es' ? (qty === 1 ? 'ticket' : 'tickets') : (qty === 1 ? 'ticket' : 'tickets')}
</span>
<Input
value={(options[key] as string | null) || ''}
onChange={(e) => updateOption(key, (e.target.value || null) as any)}
placeholder="https://www.tpago.com.py/links?alias=..."
className="flex-1"
/>
</div>
);
})}
</div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>

View File

@@ -559,6 +559,7 @@ export interface Event {
export interface Ticket { export interface Ticket {
id: string; id: string;
bookingId?: string; // Groups multiple tickets from same booking bookingId?: string; // Groups multiple tickets from same booking
bookingTicketCount?: number; // Total tickets in the booking (for per-quantity payment links)
userId: string; userId: string;
eventId: string; eventId: string;
attendeeFirstName: string; attendeeFirstName: string;
@@ -673,6 +674,10 @@ export interface PaymentWithDetails extends Payment {
export interface PaymentOptionsConfig { export interface PaymentOptionsConfig {
tpagoEnabled: boolean; tpagoEnabled: boolean;
tpagoLink?: string | null; tpagoLink?: string | null;
tpagoLink2?: string | null;
tpagoLink3?: string | null;
tpagoLink4?: string | null;
tpagoLink5?: string | null;
tpagoInstructions?: string | null; tpagoInstructions?: string | null;
tpagoInstructionsEs?: string | null; tpagoInstructionsEs?: string | null;
bankTransferEnabled: boolean; bankTransferEnabled: boolean;

View File

@@ -165,3 +165,31 @@ export function formatPrice(price: number, currency: string = 'PYG'): string {
export function formatCurrency(amount: number, currency: string = 'PYG'): string { export function formatCurrency(amount: number, currency: string = 'PYG'): string {
return formatPrice(amount, currency); return formatPrice(amount, currency);
} }
// ---------------------------------------------------------------------------
// Payment helpers
// ---------------------------------------------------------------------------
type TpagoLinkConfig = {
tpagoLink?: string | null;
tpagoLink2?: string | null;
tpagoLink3?: string | null;
tpagoLink4?: string | null;
tpagoLink5?: string | null;
};
/**
* Select the TPago payment link that matches the number of tickets being
* purchased (1-5). Each link has a fixed amount baked in, so the quantity
* determines which one to use. Falls back to the base (1-ticket) link when a
* specific quantity link isn't configured.
*/
export function getTpagoLink(
config: TpagoLinkConfig | null | undefined,
ticketCount: number
): string | null {
if (!config) return null;
const count = Math.min(Math.max(1, Math.floor(ticketCount || 1)), 5);
const key = (count <= 1 ? 'tpagoLink' : `tpagoLink${count}`) as keyof TpagoLinkConfig;
return config[key] || config.tpagoLink || null;
}