uid change
This commit is contained in:
parent
9a6a02a9e8
commit
554a573c98
@ -33,6 +33,9 @@ export type SubscribeAboInput = {
|
||||
invoiceCity?: string
|
||||
invoicePhone?: string
|
||||
invoiceEmail?: string
|
||||
uidNumber?: string
|
||||
atuNumber?: string
|
||||
taxMode?: 'standard_vat' | 'reverse_charge'
|
||||
signingCity?: string
|
||||
signatureDataUrl?: string
|
||||
// logged-in user id
|
||||
@ -99,6 +102,9 @@ export async function subscribeAbo(input: SubscribeAboInput) {
|
||||
paymentMethod: input.paymentMethod || undefined,
|
||||
invoiceByEmail: input.invoiceByEmail ?? false,
|
||||
invoiceSameAsShipping: input.invoiceSameAsShipping ?? true,
|
||||
uidNumber: input.uidNumber || undefined,
|
||||
atuNumber: input.atuNumber || undefined,
|
||||
taxMode: input.taxMode || undefined,
|
||||
signingCity: input.signingCity || undefined,
|
||||
signatureDataUrl: input.signatureDataUrl || undefined,
|
||||
}
|
||||
|
||||
@ -43,6 +43,24 @@ function hashString(value: string): number {
|
||||
return hash >>> 0
|
||||
}
|
||||
|
||||
const HOME_COUNTRY_CODE = 'AT'
|
||||
|
||||
function normalizeUid(value: unknown): string {
|
||||
if (typeof value !== 'string') return ''
|
||||
return value.replace(/\s+/g, '').toUpperCase()
|
||||
}
|
||||
|
||||
function isLikelyValidUid(value: string): boolean {
|
||||
return /^[A-Z]{2}[A-Z0-9]{4,14}$/.test(value)
|
||||
}
|
||||
|
||||
function pickFirstString(...values: unknown[]): string {
|
||||
for (const value of values) {
|
||||
if (typeof value === 'string' && value.trim() !== '') return value.trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export default function SummaryPage() {
|
||||
const router = useRouter();
|
||||
const { coffees, loading, error } = useActiveCoffees();
|
||||
@ -75,6 +93,7 @@ export default function SummaryPage() {
|
||||
invoiceCity: '',
|
||||
invoicePhone: '',
|
||||
invoiceEmail: '',
|
||||
uidNumber: '',
|
||||
signingCity: '',
|
||||
});
|
||||
const [showThanks, setShowThanks] = useState(false);
|
||||
@ -90,12 +109,27 @@ export default function SummaryPage() {
|
||||
const templateVariableNames = useMemo(() => extractTemplateVariables(contractHtml), [contractHtml])
|
||||
const templateVariableNamesKey = useMemo(() => templateVariableNames.join('|'), [templateVariableNames])
|
||||
const [contractVariables, setContractVariables] = useState<Record<string, string>>({})
|
||||
const isCompanyCustomer = user?.userType === 'company' || user?.user_type === 'company'
|
||||
|
||||
const profileUidNumber = useMemo(() => normalizeUid(pickFirstString(
|
||||
user?.uidNumber,
|
||||
user?.uid_number,
|
||||
user?.atuNumber,
|
||||
user?.atu_number,
|
||||
user?.companyProfile?.uid_number,
|
||||
user?.companyProfile?.atu_number
|
||||
)), [user])
|
||||
|
||||
const enteredUidNumber = useMemo(() => normalizeUid(form.uidNumber), [form.uidNumber])
|
||||
const effectiveUidNumber = enteredUidNumber || profileUidNumber
|
||||
const hasValidCompanyUid = isCompanyCustomer && isLikelyValidUid(effectiveUidNumber)
|
||||
const isForeignInvoiceCountry = form.country.toUpperCase() !== HOME_COUNTRY_CODE
|
||||
const isReverseCharge = isCompanyCustomer && hasValidCompanyUid && isForeignInvoiceCountry
|
||||
|
||||
// Auto-compute contract variables from form state for preview
|
||||
useEffect(() => {
|
||||
if (!templateVariableNamesKey) return
|
||||
const fullName = `${form.firstName} ${form.lastName}`.trim()
|
||||
const isCompany = user?.userType === 'company' || user?.user_type === 'company'
|
||||
const invoiceSame = form.invoiceSameAsShipping
|
||||
|
||||
const computed: Record<string, string> = {
|
||||
@ -103,8 +137,8 @@ export default function SummaryPage() {
|
||||
currentDate: new Date().toLocaleDateString('de-AT', { day: '2-digit', month: '2-digit', year: 'numeric' }),
|
||||
recipientName: fullName,
|
||||
recipientAddress: `${form.street}, ${form.postalCode} ${form.city}`.trim(),
|
||||
shippingCustomerClass: isCompany ? '' : 'checked',
|
||||
shippingCompanyClass: isCompany ? 'checked' : '',
|
||||
shippingCustomerClass: isCompanyCustomer ? '' : 'checked',
|
||||
shippingCompanyClass: isCompanyCustomer ? 'checked' : '',
|
||||
shippingFullName: fullName,
|
||||
shippingStreet: form.street,
|
||||
shippingPostalCode: form.postalCode,
|
||||
@ -112,8 +146,8 @@ export default function SummaryPage() {
|
||||
shippingPhone: form.phone,
|
||||
shippingEmail: form.email,
|
||||
invoiceSameAsShippingMark: invoiceSame ? '✓' : '',
|
||||
invoiceCompanyClass: isCompany ? 'checked' : '',
|
||||
invoiceCustomerClass: isCompany ? '' : 'checked',
|
||||
invoiceCompanyClass: isCompanyCustomer ? 'checked' : '',
|
||||
invoiceCustomerClass: isCompanyCustomer ? '' : 'checked',
|
||||
invoiceFullName: invoiceSame ? fullName : form.invoiceFullName,
|
||||
invoiceStreet: invoiceSame ? form.street : form.invoiceStreet,
|
||||
invoicePostalCode: invoiceSame ? form.postalCode : form.invoicePostalCode,
|
||||
@ -122,10 +156,10 @@ export default function SummaryPage() {
|
||||
invoiceEmail: invoiceSame ? form.email : form.invoiceEmail,
|
||||
fnCheckedClass: '',
|
||||
fnNumber: '',
|
||||
atuCheckedClass: '',
|
||||
atuNumber: '',
|
||||
entrepreneurClass: isCompany ? 'checked' : '',
|
||||
consumerClass: isCompany ? '' : 'checked',
|
||||
atuCheckedClass: hasValidCompanyUid ? 'checked' : '',
|
||||
atuNumber: effectiveUidNumber,
|
||||
entrepreneurClass: isCompanyCustomer ? 'checked' : '',
|
||||
consumerClass: isCompanyCustomer ? '' : 'checked',
|
||||
paymentSepaClass: form.paymentMethod === 'sepa' ? 'checked' : '',
|
||||
paymentCardClass: form.paymentMethod === 'card' ? 'checked' : '',
|
||||
paymentSofortClass: form.paymentMethod === 'sofort' ? 'checked' : '',
|
||||
@ -134,7 +168,7 @@ export default function SummaryPage() {
|
||||
fullName,
|
||||
}
|
||||
setContractVariables(computed)
|
||||
}, [templateVariableNamesKey, form, user, signatureDataUrl])
|
||||
}, [templateVariableNamesKey, form, signatureDataUrl, effectiveUidNumber, hasValidCompanyUid, isCompanyCustomer])
|
||||
|
||||
const populatedContractHtml = useMemo(() => {
|
||||
if (!contractHtml) return null
|
||||
@ -458,8 +492,9 @@ export default function SummaryPage() {
|
||||
[totalPrice, shippingFee]
|
||||
);
|
||||
|
||||
const taxAmount = useMemo(() => totalPrice * taxRate, [totalPrice, taxRate]);
|
||||
const taxAmountWithShipping = useMemo(() => netWithShipping * taxRate, [netWithShipping, taxRate]);
|
||||
const effectiveTaxRate = isReverseCharge ? 0 : taxRate
|
||||
const taxAmount = useMemo(() => totalPrice * effectiveTaxRate, [totalPrice, effectiveTaxRate]);
|
||||
const taxAmountWithShipping = useMemo(() => netWithShipping * effectiveTaxRate, [netWithShipping, effectiveTaxRate]);
|
||||
const totalWithTax = useMemo(() => netWithShipping + taxAmountWithShipping, [netWithShipping, taxAmountWithShipping]);
|
||||
|
||||
const handleInput = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
@ -478,23 +513,24 @@ export default function SummaryPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const pick = (...values: any[]) => {
|
||||
for (const value of values) {
|
||||
if (typeof value === 'string' && value.trim() !== '') return value.trim();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
setSubmitError(null);
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
firstName: pick(user.firstName, user.firstname, user.givenName, user.first_name) || prev.firstName,
|
||||
lastName: pick(user.lastName, user.lastname, user.familyName, user.last_name) || prev.lastName,
|
||||
email: pick(user.email, user.mail) || prev.email,
|
||||
street: pick(user.street, user.addressStreet, user.address?.street, user.address_line_1) || prev.street,
|
||||
postalCode: pick(user.postalCode, user.zipCode, user.zip, user.addressPostalCode, user.address?.postalCode) || prev.postalCode,
|
||||
city: pick(user.city, user.addressCity, user.town, user.address?.city) || prev.city,
|
||||
country: (pick(user.country, user.countryCode, user.addressCountry, user.address?.country) || prev.country).toUpperCase(),
|
||||
firstName: pickFirstString(user.firstName, user.firstname, user.givenName, user.first_name) || prev.firstName,
|
||||
lastName: pickFirstString(user.lastName, user.lastname, user.familyName, user.last_name) || prev.lastName,
|
||||
email: pickFirstString(user.email, user.mail) || prev.email,
|
||||
street: pickFirstString(user.street, user.addressStreet, user.address?.street, user.address_line_1) || prev.street,
|
||||
postalCode: pickFirstString(user.postalCode, user.zipCode, user.zip, user.addressPostalCode, user.address?.postalCode) || prev.postalCode,
|
||||
city: pickFirstString(user.city, user.addressCity, user.town, user.address?.city) || prev.city,
|
||||
country: (pickFirstString(user.country, user.countryCode, user.addressCountry, user.address?.country) || prev.country).toUpperCase(),
|
||||
uidNumber: normalizeUid(pickFirstString(
|
||||
user.uidNumber,
|
||||
user.uid_number,
|
||||
user.atuNumber,
|
||||
user.atu_number,
|
||||
user.companyProfile?.uid_number,
|
||||
user.companyProfile?.atu_number
|
||||
) || prev.uidNumber),
|
||||
}));
|
||||
};
|
||||
|
||||
@ -576,6 +612,9 @@ export default function SummaryPage() {
|
||||
} : {}),
|
||||
signingCity: form.signingCity.trim() || undefined,
|
||||
signatureDataUrl: signatureDataUrl || undefined,
|
||||
uidNumber: effectiveUidNumber || undefined,
|
||||
atuNumber: effectiveUidNumber || undefined,
|
||||
taxMode: isReverseCharge ? 'reverse_charge' : 'standard_vat',
|
||||
referred_by: typeof currentUserId === 'number' ? currentUserId : undefined,
|
||||
}
|
||||
console.info('[SummaryPage] subscribeAbo payload:', payload)
|
||||
@ -730,10 +769,30 @@ export default function SummaryPage() {
|
||||
{/* Invoice address */}
|
||||
<div className="mt-6 border-t border-gray-200 pt-4">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-3">Invoice address</h3>
|
||||
{isCompanyCustomer && (
|
||||
<div className="mb-3 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-900">
|
||||
Unternehmer mit gueltiger UID und Rechnungsland ausserhalb von {HOME_COUNTRY_CODE} werden per Reverse Charge ohne ausgewiesene MwSt verrechnet.
|
||||
</div>
|
||||
)}
|
||||
<label className="flex items-center gap-2 text-sm mb-3">
|
||||
<input type="checkbox" name="invoiceSameAsShipping" checked={form.invoiceSameAsShipping} onChange={handleCheckbox} className="accent-[#1C2B4A]" />
|
||||
Same as shipping address
|
||||
</label>
|
||||
{isCompanyCustomer && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">UID Number (optional)</label>
|
||||
<input
|
||||
name="uidNumber"
|
||||
value={form.uidNumber}
|
||||
onChange={handleInput}
|
||||
placeholder="z.B. SI12345678"
|
||||
className="w-full rounded border px-3 py-2 bg-white border-gray-300 uppercase focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-600">
|
||||
Ohne gueltige UID wird die Rechnung mit normaler MwSt erstellt.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!form.invoiceSameAsShipping && (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="sm:col-span-2">
|
||||
@ -902,13 +961,18 @@ export default function SummaryPage() {
|
||||
<span className="text-lg font-extrabold tracking-tight text-[#1C2B4A]">€{netWithShipping.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm">Tax ({(taxRate * 100).toFixed(1)}%)</span>
|
||||
<span className="text-sm">{isReverseCharge ? 'Tax (Reverse Charge)' : `Tax (${(effectiveTaxRate * 100).toFixed(1)}%)`}</span>
|
||||
<span className="text-sm font-medium">€{taxAmountWithShipping.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-semibold">Total incl. tax</span>
|
||||
<span className="text-xl font-extrabold text-[#1C2B4A]">€{totalWithTax.toFixed(2)}</span>
|
||||
</div>
|
||||
{isReverseCharge && (
|
||||
<div className="mt-2 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-900">
|
||||
Reverse Charge aktiv: gueltige UID und auslaendisches Rechnungsland erkannt.
|
||||
</div>
|
||||
)}
|
||||
{/* Validation summary (refined design) */}
|
||||
<div className="mt-2 text-xs text-gray-700">
|
||||
Selected: {totalCapsules} capsules ({totalPacks} packs of 10). Target: {selectedPlanCapsules} capsules ({requiredPacks} packs).
|
||||
|
||||
@ -15,7 +15,8 @@ interface CompanyProfileData {
|
||||
companyPhone: string
|
||||
contactPersonName: string
|
||||
contactPersonPhone: string
|
||||
vatNumber: string
|
||||
registrationNumber: string
|
||||
uidNumber: string
|
||||
street: string
|
||||
postalCode: string
|
||||
city: string
|
||||
@ -44,7 +45,8 @@ const init: CompanyProfileData = {
|
||||
companyPhone: '',
|
||||
contactPersonName: '',
|
||||
contactPersonPhone: '',
|
||||
vatNumber: '',
|
||||
registrationNumber: '',
|
||||
uidNumber: '',
|
||||
street: '',
|
||||
postalCode: '',
|
||||
city: '',
|
||||
@ -216,7 +218,8 @@ export default function CompanyAdditionalInformationPage() {
|
||||
companyPhone: profile?.phone || me?.companyPhone || prev.companyPhone,
|
||||
contactPersonName: profile?.contact_person_name || me?.contactPersonName || prev.contactPersonName,
|
||||
contactPersonPhone: profile?.contact_person_phone || me?.contactPersonPhone || prev.contactPersonPhone,
|
||||
vatNumber: profile?.registration_number || prev.vatNumber,
|
||||
registrationNumber: profile?.registration_number || prev.registrationNumber,
|
||||
uidNumber: profile?.uid_number || profile?.atu_number || prev.uidNumber,
|
||||
street: profile?.address || prev.street,
|
||||
postalCode: profile?.zip_code || prev.postalCode,
|
||||
city: profile?.city || prev.city,
|
||||
@ -281,7 +284,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
const validate = () => {
|
||||
const required: (keyof CompanyProfileData)[] = [
|
||||
'companyName','companyEmail','companyPhone','contactPersonName','contactPersonPhone',
|
||||
'vatNumber','street','postalCode','city','country','accountHolder','iban'
|
||||
'street','postalCode','city','country','accountHolder','iban'
|
||||
]
|
||||
for (const k of required) {
|
||||
if (!form[k].trim()) {
|
||||
@ -414,7 +417,9 @@ export default function CompanyAdditionalInformationPage() {
|
||||
zip_code: form.postalCode, // Backend expects 'zip_code'
|
||||
city: form.city,
|
||||
country: form.country,
|
||||
registrationNumber: form.vatNumber, // Map VAT number to registration number
|
||||
registrationNumber: form.registrationNumber || undefined,
|
||||
uidNumber: form.uidNumber || undefined,
|
||||
atuNumber: form.uidNumber || undefined,
|
||||
businessType: 'company', // Default business type
|
||||
branch: null, // Not collected in form, set to null
|
||||
numberOfEmployees: null, // Not collected in form, set to null
|
||||
@ -580,15 +585,26 @@ export default function CompanyAdditionalInformationPage() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
VAT / Reg No. *
|
||||
Registration Number (optional)
|
||||
</label>
|
||||
<input
|
||||
name="vatNumber"
|
||||
value={form.vatNumber}
|
||||
name="registrationNumber"
|
||||
value={form.registrationNumber}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g. DE123456789"
|
||||
placeholder="e.g. FN123456a"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
UID Number (optional)
|
||||
</label>
|
||||
<input
|
||||
name="uidNumber"
|
||||
value={form.uidNumber}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g. ATU12345678"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2 lg:col-span-3">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user