profit-planet-frontend/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts
seaznCode 11e3e384bd feat: add customer fields to subscription and update invoice handling
- 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.
2026-03-16 20:35:36 +01:00

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[]
}