- Added new customer fields in SubscribeAboInput type including phone, recipient contract name, and invoice details. - Updated subscription validation to include new required fields. - Modified the subscribeAbo function to handle new customer and invoice fields. - Enhanced the SummaryPage component to manage new form fields for invoice address and payment method. - Removed the toggle for "For myself / For someone else" as the logic has been simplified. - Updated contract template handling to reflect changes in the form data structure.
171 lines
6.9 KiB
TypeScript
171 lines
6.9 KiB
TypeScript
import { authFetch } from '../../../utils/authFetch'
|
|
|
|
export type SubscribeAboItem = { coffeeId: string | number; quantity?: number }
|
|
export type SubscribeAboInput = {
|
|
coffeeId?: string | number // optional when items provided
|
|
items?: SubscribeAboItem[] // NEW: whole order in one call
|
|
billing_interval?: string
|
|
interval_count?: number
|
|
is_auto_renew?: boolean
|
|
is_for_self?: boolean
|
|
target_user_id?: number
|
|
recipient_name?: string
|
|
recipient_email?: string
|
|
// Customer fields
|
|
firstName?: string
|
|
lastName?: string
|
|
email?: string
|
|
street?: string
|
|
postalCode?: string
|
|
city?: string
|
|
country?: string
|
|
frequency?: string
|
|
// New contract / contact fields
|
|
phone?: string
|
|
recipientContractName?: string
|
|
recipientAddress?: string
|
|
paymentMethod?: string
|
|
invoiceByEmail?: boolean
|
|
invoiceSameAsShipping?: boolean
|
|
invoiceFullName?: string
|
|
invoiceStreet?: string
|
|
invoicePostalCode?: string
|
|
invoiceCity?: string
|
|
invoicePhone?: string
|
|
invoiceEmail?: string
|
|
signingCity?: string
|
|
signatureDataUrl?: string
|
|
// logged-in user id
|
|
referred_by?: number | string
|
|
}
|
|
|
|
type Abonement = any
|
|
type HistoryEvent = any
|
|
|
|
const apiBase = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, '')
|
|
|
|
const parseJson = async (res: Response) => {
|
|
const ct = res.headers.get('content-type') || ''
|
|
const isJson = ct.includes('application/json')
|
|
const json = isJson ? await res.json().catch(() => ({})) : null
|
|
return { json, isJson }
|
|
}
|
|
|
|
export async function subscribeAbo(input: SubscribeAboInput) {
|
|
const hasItems = Array.isArray(input.items) && input.items.length > 0
|
|
if (!hasItems && !input.coffeeId) throw new Error('coffeeId is required')
|
|
|
|
const isForSelf = input.is_for_self ?? true
|
|
if (!isForSelf && (!input.recipient_email || input.recipient_email.trim() === '')) {
|
|
throw new Error('recipient_email is required when subscription is for someone else.')
|
|
}
|
|
|
|
// NEW: validate customer fields (required in UI)
|
|
const requiredFields = ['firstName','lastName','email','street','postalCode','city','country'] as const
|
|
const missing = requiredFields.filter(k => {
|
|
const v = (input as any)[k]
|
|
return typeof v !== 'string' || v.trim() === ''
|
|
})
|
|
if (missing.length) {
|
|
throw new Error(`Missing required fields: ${missing.join(', ')}`)
|
|
}
|
|
|
|
const body: any = {
|
|
billing_interval: input.billing_interval ?? 'month',
|
|
interval_count: input.interval_count ?? 1,
|
|
is_auto_renew: input.is_auto_renew ?? true,
|
|
is_for_self: isForSelf,
|
|
// Customer fields
|
|
firstName: input.firstName,
|
|
lastName: input.lastName,
|
|
email: input.email,
|
|
street: input.street,
|
|
postalCode: input.postalCode,
|
|
city: input.city,
|
|
country: input.country?.toUpperCase?.() ?? input.country,
|
|
frequency: input.frequency,
|
|
// New contract / contact fields
|
|
phone: input.phone || undefined,
|
|
recipientContractName: input.recipientContractName || undefined,
|
|
recipientAddress: input.recipientAddress || undefined,
|
|
paymentMethod: input.paymentMethod || undefined,
|
|
invoiceByEmail: input.invoiceByEmail ?? false,
|
|
invoiceSameAsShipping: input.invoiceSameAsShipping ?? true,
|
|
signingCity: input.signingCity || undefined,
|
|
signatureDataUrl: input.signatureDataUrl || undefined,
|
|
}
|
|
|
|
// Include invoice address fields when not same as shipping
|
|
if (!body.invoiceSameAsShipping) {
|
|
body.invoiceFullName = input.invoiceFullName || undefined
|
|
body.invoiceStreet = input.invoiceStreet || undefined
|
|
body.invoicePostalCode = input.invoicePostalCode || undefined
|
|
body.invoiceCity = input.invoiceCity || undefined
|
|
body.invoicePhone = input.invoicePhone || undefined
|
|
body.invoiceEmail = input.invoiceEmail || undefined
|
|
}
|
|
if (hasItems) {
|
|
body.items = input.items!.map(i => ({
|
|
coffeeId: i.coffeeId,
|
|
quantity: i.quantity != null ? i.quantity : 1,
|
|
}))
|
|
// NEW: enforce supported package sizes
|
|
const sumPacks = body.items.reduce((s: number, it: any) => s + Number(it.quantity || 0), 0)
|
|
if (sumPacks !== 6 && sumPacks !== 12) {
|
|
console.warn('[subscribeAbo] Invalid pack total:', sumPacks, 'expected 6 or 12')
|
|
throw new Error('Order must contain exactly 6 packs (60 capsules) or 12 packs (120 capsules).')
|
|
}
|
|
} else {
|
|
body.coffeeId = input.coffeeId
|
|
// single-item legacy path — backend expects bundle, prefer items usage
|
|
}
|
|
// NEW: always include available recipient fields and target_user_id when provided
|
|
if (input.target_user_id != null) body.target_user_id = input.target_user_id
|
|
if (!isForSelf && input.recipient_email) body.recipient_email = input.recipient_email
|
|
if (!isForSelf && input.recipient_name) body.recipient_name = input.recipient_name
|
|
// NEW: always include referred_by if provided
|
|
if (input.referred_by != null) body.referred_by = input.referred_by
|
|
|
|
const url = `${apiBase}/api/abonements/subscribe`
|
|
console.info('[subscribeAbo] POST', url, { body })
|
|
// NEW: explicit JSON preview that matches the actual request body
|
|
console.info('[subscribeAbo] Body JSON:', JSON.stringify(body))
|
|
const res = await authFetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify(body),
|
|
credentials: 'include',
|
|
})
|
|
const { json } = await parseJson(res)
|
|
console.info('[subscribeAbo] Response', res.status, json)
|
|
if (!res.ok || !json?.success) throw new Error(json?.message || `Subscribe failed: ${res.status}`)
|
|
return json.data as Abonement
|
|
}
|
|
|
|
async function postAction(url: string) {
|
|
const res = await authFetch(url, { method: 'POST', headers: { Accept: 'application/json' }, credentials: 'include' })
|
|
const { json } = await parseJson(res)
|
|
if (!res.ok || !json?.success) throw new Error(json?.message || `Request failed: ${res.status}`)
|
|
return json.data as Abonement
|
|
}
|
|
|
|
export const pauseAbo = (id: string | number) => postAction(`${apiBase}/abonements/${id}/pause`)
|
|
export const resumeAbo = (id: string | number) => postAction(`${apiBase}/abonements/${id}/resume`)
|
|
export const cancelAbo = (id: string | number) => postAction(`${apiBase}/abonements/${id}/cancel`)
|
|
export const renewAbo = (id: string | number) => postAction(`${apiBase}/admin/abonements/${id}/renew`)
|
|
|
|
export async function getMyAbonements(status?: string) {
|
|
const qs = status ? `?status=${encodeURIComponent(status)}` : ''
|
|
const res = await authFetch(`${apiBase}/abonements/mine${qs}`, { method: 'GET', headers: { Accept: 'application/json' }, credentials: 'include' })
|
|
const { json } = await parseJson(res)
|
|
if (!res.ok || !json?.success) throw new Error(json?.message || `Fetch failed: ${res.status}`)
|
|
return (json.data || []) as Abonement[]
|
|
}
|
|
|
|
export async function getAboHistory(id: string | number) {
|
|
const res = await authFetch(`${apiBase}/abonements/${id}/history`, { method: 'GET', headers: { Accept: 'application/json' }, credentials: 'include' })
|
|
const { json } = await parseJson(res)
|
|
if (!res.ok || !json?.success) throw new Error(json?.message || `History failed: ${res.status}`)
|
|
return (json.data || []) as HistoryEvent[]
|
|
}
|