chore: germantax customer project
This commit is contained in:
60
packages/functions/src/templates/annual-budget-approval.ts
Normal file
60
packages/functions/src/templates/annual-budget-approval.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { z } from 'zod'
|
||||
import type { ResolutionTemplate } from './index.js'
|
||||
import {
|
||||
baseDocument,
|
||||
escapeHtml,
|
||||
formatDateDe,
|
||||
formatEur,
|
||||
renderMembers,
|
||||
} from './shared.js'
|
||||
|
||||
const schema = z.object({
|
||||
fiscalYear: z.string().min(4),
|
||||
totalBudget: z.coerce.number().nonnegative(),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
|
||||
export const annualBudgetApproval: ResolutionTemplate = {
|
||||
key: 'annual_budget_approval',
|
||||
titleDe: 'Beschluss zur Genehmigung des Jahresbudgets',
|
||||
titleEn: 'Annual budget approval',
|
||||
descriptionDe:
|
||||
'Gesellschafterbeschluss zur Feststellung und Genehmigung des Jahresbudgets der Gesellschaft.',
|
||||
schema,
|
||||
fields: [
|
||||
{ key: 'fiscalYear', labelDe: 'Geschäftsjahr', labelEn: 'Fiscal year', type: 'text', required: true },
|
||||
{ key: 'totalBudget', labelDe: 'Gesamtbudget (EUR)', labelEn: 'Total budget (EUR)', type: 'currency', required: true },
|
||||
{ key: 'notes', labelDe: 'Anmerkungen', labelEn: 'Notes', type: 'textarea' },
|
||||
],
|
||||
render: ({ values, company, members, createdBy, date }) => {
|
||||
const v = schema.parse(values)
|
||||
const body = `
|
||||
<h1>Gesellschafterbeschluss</h1>
|
||||
<div class="meta">
|
||||
${escapeHtml(company.name)}${company.registryNo ? `, ${escapeHtml(company.registryNo)}` : ''}<br/>
|
||||
${company.address ? escapeHtml(company.address) + '<br/>' : ''}
|
||||
Beschlussdatum: ${escapeHtml(formatDateDe(date))}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gegenstand des Beschlusses</h2>
|
||||
<p>Genehmigung des Jahresbudgets für das Geschäftsjahr <strong>${escapeHtml(v.fiscalYear)}</strong>.</p>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Beschluss</h2>
|
||||
<p>Die Gesellschafter beschließen einstimmig, das Jahresbudget in Höhe von
|
||||
<strong>${escapeHtml(formatEur(v.totalBudget))}</strong> für das Geschäftsjahr
|
||||
${escapeHtml(v.fiscalYear)} festzustellen und zu genehmigen.</p>
|
||||
${v.notes ? `<p><em>${escapeHtml(v.notes)}</em></p>` : ''}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gesellschafter</h2>
|
||||
${renderMembers(members)}
|
||||
</div>
|
||||
<div class="section signature">
|
||||
<div class="line">${escapeHtml(createdBy.displayName)}<br/><small>${escapeHtml(createdBy.email)}</small></div>
|
||||
<div class="line">Ort, Datum</div>
|
||||
</div>
|
||||
`
|
||||
return baseDocument('Beschluss – Jahresbudget', body)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { z } from 'zod'
|
||||
import type { ResolutionTemplate } from './index.js'
|
||||
import {
|
||||
baseDocument,
|
||||
escapeHtml,
|
||||
formatDateDe,
|
||||
formatEur,
|
||||
renderMembers,
|
||||
} from './shared.js'
|
||||
|
||||
const schema = z.object({
|
||||
fiscalYear: z.string().min(4),
|
||||
balanceSheetTotal: z.coerce.number().nonnegative(),
|
||||
netResult: z.coerce.number(),
|
||||
approvalDate: z.string().min(8),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
|
||||
export const annualFinancialsApproval: ResolutionTemplate = {
|
||||
key: 'annual_financials_approval',
|
||||
titleDe: 'Feststellung des Jahresabschlusses',
|
||||
titleEn: 'Annual financial statements approval',
|
||||
descriptionDe:
|
||||
'Gesellschafterbeschluss zur Feststellung des Jahresabschlusses gemäß § 46 Nr. 1 GmbHG.',
|
||||
schema,
|
||||
fields: [
|
||||
{ key: 'fiscalYear', labelDe: 'Geschäftsjahr', labelEn: 'Fiscal year', type: 'text', required: true },
|
||||
{ key: 'balanceSheetTotal', labelDe: 'Bilanzsumme (EUR)', labelEn: 'Balance sheet total (EUR)', type: 'currency', required: true },
|
||||
{ key: 'netResult', labelDe: 'Jahresergebnis (EUR)', labelEn: 'Net result (EUR)', type: 'currency', required: true },
|
||||
{ key: 'approvalDate', labelDe: 'Datum der Feststellung', labelEn: 'Approval date', type: 'date', required: true },
|
||||
{ key: 'notes', labelDe: 'Anmerkungen', labelEn: 'Notes', type: 'textarea' },
|
||||
],
|
||||
render: ({ values, company, members, createdBy, date }) => {
|
||||
const v = schema.parse(values)
|
||||
const profitOrLoss = v.netResult >= 0 ? 'Jahresüberschuss' : 'Jahresfehlbetrag'
|
||||
const body = `
|
||||
<h1>Gesellschafterbeschluss</h1>
|
||||
<div class="meta">
|
||||
${escapeHtml(company.name)}${company.registryNo ? `, ${escapeHtml(company.registryNo)}` : ''}<br/>
|
||||
${company.address ? escapeHtml(company.address) + '<br/>' : ''}
|
||||
Beschlussdatum: ${escapeHtml(formatDateDe(date))}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gegenstand des Beschlusses</h2>
|
||||
<p>Feststellung des Jahresabschlusses für das Geschäftsjahr
|
||||
<strong>${escapeHtml(v.fiscalYear)}</strong> gemäß § 46 Nr. 1 GmbHG.</p>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Beschluss</h2>
|
||||
<p>Die Gesellschafter stellen den Jahresabschluss zum
|
||||
<strong>${escapeHtml(formatDateDe(v.approvalDate))}</strong> fest mit einer
|
||||
Bilanzsumme von <strong>${escapeHtml(formatEur(v.balanceSheetTotal))}</strong>
|
||||
und einem ${escapeHtml(profitOrLoss)} von
|
||||
<strong>${escapeHtml(formatEur(v.netResult))}</strong>.</p>
|
||||
${v.notes ? `<p><em>${escapeHtml(v.notes)}</em></p>` : ''}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gesellschafter</h2>
|
||||
${renderMembers(members)}
|
||||
</div>
|
||||
<div class="section signature">
|
||||
<div class="line">${escapeHtml(createdBy.displayName)}<br/><small>${escapeHtml(createdBy.email)}</small></div>
|
||||
<div class="line">Ort, Datum</div>
|
||||
</div>
|
||||
`
|
||||
return baseDocument('Beschluss – Feststellung Jahresabschluss', body)
|
||||
},
|
||||
}
|
||||
77
packages/functions/src/templates/capital-increase.ts
Normal file
77
packages/functions/src/templates/capital-increase.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { z } from 'zod'
|
||||
import type { ResolutionTemplate } from './index.js'
|
||||
import {
|
||||
baseDocument,
|
||||
escapeHtml,
|
||||
formatDateDe,
|
||||
formatEur,
|
||||
renderMembers,
|
||||
} from './shared.js'
|
||||
|
||||
const schema = z.object({
|
||||
currentCapital: z.coerce.number().nonnegative(),
|
||||
increaseAmount: z.coerce.number().positive(),
|
||||
newCapital: z.coerce.number().positive(),
|
||||
contributionType: z.enum(['cash', 'in_kind']).default('cash'),
|
||||
effectiveDate: z.string().min(8),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
|
||||
const contributionLabel = (type: 'cash' | 'in_kind'): string =>
|
||||
type === 'cash' ? 'Bareinlage' : 'Sacheinlage'
|
||||
|
||||
export const capitalIncrease: ResolutionTemplate = {
|
||||
key: 'capital_increase',
|
||||
titleDe: 'Beschluss zur Kapitalerhöhung',
|
||||
titleEn: 'Capital increase',
|
||||
descriptionDe:
|
||||
'Gesellschafterbeschluss über die Erhöhung des Stammkapitals gemäß §§ 55 ff. GmbHG. Beurkundungspflichtig.',
|
||||
schema,
|
||||
fields: [
|
||||
{ key: 'currentCapital', labelDe: 'Bisheriges Stammkapital (EUR)', labelEn: 'Current share capital (EUR)', type: 'currency', required: true },
|
||||
{ key: 'increaseAmount', labelDe: 'Erhöhungsbetrag (EUR)', labelEn: 'Increase amount (EUR)', type: 'currency', required: true },
|
||||
{ key: 'newCapital', labelDe: 'Neues Stammkapital (EUR)', labelEn: 'New share capital (EUR)', type: 'currency', required: true },
|
||||
{ key: 'contributionType', labelDe: 'Art der Einlage (cash | in_kind)', labelEn: 'Contribution type (cash | in_kind)', type: 'text', required: true },
|
||||
{ key: 'effectiveDate', labelDe: 'Wirksam ab', labelEn: 'Effective date', type: 'date', required: true },
|
||||
{ key: 'notes', labelDe: 'Anmerkungen', labelEn: 'Notes', type: 'textarea' },
|
||||
],
|
||||
render: ({ values, company, members, createdBy, date }) => {
|
||||
const v = schema.parse(values)
|
||||
const body = `
|
||||
<h1>Gesellschafterbeschluss</h1>
|
||||
<div class="meta">
|
||||
${escapeHtml(company.name)}${company.registryNo ? `, ${escapeHtml(company.registryNo)}` : ''}<br/>
|
||||
${company.address ? escapeHtml(company.address) + '<br/>' : ''}
|
||||
Beschlussdatum: ${escapeHtml(formatDateDe(date))}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gegenstand des Beschlusses</h2>
|
||||
<p>Erhöhung des Stammkapitals der Gesellschaft gemäß §§ 55 ff. GmbHG.
|
||||
Dieser Beschluss bedarf zu seiner Wirksamkeit der notariellen Beurkundung
|
||||
sowie der Eintragung in das Handelsregister.</p>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Beschluss</h2>
|
||||
<ul>
|
||||
<li>Bisheriges Stammkapital: <strong>${escapeHtml(formatEur(v.currentCapital))}</strong></li>
|
||||
<li>Erhöhung um: <strong>${escapeHtml(formatEur(v.increaseAmount))}</strong></li>
|
||||
<li>Neues Stammkapital: <strong>${escapeHtml(formatEur(v.newCapital))}</strong></li>
|
||||
<li>Art der Einlage: <strong>${escapeHtml(contributionLabel(v.contributionType))}</strong></li>
|
||||
<li>Wirksam ab: <strong>${escapeHtml(formatDateDe(v.effectiveDate))}</strong></li>
|
||||
</ul>
|
||||
<p>Die Geschäftsführung wird angewiesen, die Kapitalerhöhung unverzüglich
|
||||
zur Eintragung in das Handelsregister anzumelden.</p>
|
||||
${v.notes ? `<p><em>${escapeHtml(v.notes)}</em></p>` : ''}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gesellschafter</h2>
|
||||
${renderMembers(members)}
|
||||
</div>
|
||||
<div class="section signature">
|
||||
<div class="line">${escapeHtml(createdBy.displayName)}<br/><small>${escapeHtml(createdBy.email)}</small></div>
|
||||
<div class="line">Ort, Datum</div>
|
||||
</div>
|
||||
`
|
||||
return baseDocument('Beschluss – Kapitalerhöhung', body)
|
||||
},
|
||||
}
|
||||
49
packages/functions/src/templates/index.ts
Normal file
49
packages/functions/src/templates/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { z } from 'zod'
|
||||
import { annualBudgetApproval } from './annual-budget-approval.js'
|
||||
import { mdAppointment } from './md-appointment.js'
|
||||
import { annualFinancialsApproval } from './annual-financials-approval.js'
|
||||
import { profitDistribution } from './profit-distribution.js'
|
||||
import { mdDismissal } from './md-dismissal.js'
|
||||
import { capitalIncrease } from './capital-increase.js'
|
||||
import { shareTransferApproval } from './share-transfer-approval.js'
|
||||
|
||||
export interface ResolutionTemplate {
|
||||
key: string
|
||||
titleDe: string
|
||||
titleEn: string
|
||||
descriptionDe: string
|
||||
schema: z.ZodTypeAny
|
||||
fields: TemplateField[]
|
||||
render: (ctx: TemplateRenderContext) => string
|
||||
}
|
||||
|
||||
export interface TemplateField {
|
||||
key: string
|
||||
labelDe: string
|
||||
labelEn: string
|
||||
type: 'text' | 'textarea' | 'number' | 'date' | 'currency'
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export interface TemplateRenderContext {
|
||||
values: Record<string, unknown>
|
||||
company: { name: string; registryNo?: string | null; address?: string | null }
|
||||
members: Array<{ displayName: string; role: string; shares: number | null }>
|
||||
createdBy: { displayName: string; email: string }
|
||||
date: string
|
||||
}
|
||||
|
||||
const registry: Record<string, ResolutionTemplate> = {
|
||||
[annualBudgetApproval.key]: annualBudgetApproval,
|
||||
[annualFinancialsApproval.key]: annualFinancialsApproval,
|
||||
[profitDistribution.key]: profitDistribution,
|
||||
[mdAppointment.key]: mdAppointment,
|
||||
[mdDismissal.key]: mdDismissal,
|
||||
[capitalIncrease.key]: capitalIncrease,
|
||||
[shareTransferApproval.key]: shareTransferApproval,
|
||||
}
|
||||
|
||||
export const listTemplates = (): ResolutionTemplate[] => Object.values(registry)
|
||||
|
||||
export const getTemplate = (key: string): ResolutionTemplate | undefined =>
|
||||
registry[key]
|
||||
66
packages/functions/src/templates/md-appointment.ts
Normal file
66
packages/functions/src/templates/md-appointment.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { z } from 'zod'
|
||||
import type { ResolutionTemplate } from './index.js'
|
||||
import {
|
||||
baseDocument,
|
||||
escapeHtml,
|
||||
formatDateDe,
|
||||
renderMembers,
|
||||
} from './shared.js'
|
||||
|
||||
const schema = z.object({
|
||||
appointeeName: z.string().min(2),
|
||||
appointeeAddress: z.string().min(2),
|
||||
effectiveDate: z.string().min(8),
|
||||
soleRepresentation: z.coerce.boolean().optional(),
|
||||
selfDealingExempt: z.coerce.boolean().optional(),
|
||||
})
|
||||
|
||||
export const mdAppointment: ResolutionTemplate = {
|
||||
key: 'md_appointment',
|
||||
titleDe: 'Beschluss zur Bestellung eines Geschäftsführers',
|
||||
titleEn: 'Managing director appointment',
|
||||
descriptionDe:
|
||||
'Gesellschafterbeschluss zur Bestellung einer Person zum Geschäftsführer der Gesellschaft.',
|
||||
schema,
|
||||
fields: [
|
||||
{ key: 'appointeeName', labelDe: 'Name des Geschäftsführers', labelEn: 'Appointee name', type: 'text', required: true },
|
||||
{ key: 'appointeeAddress', labelDe: 'Anschrift', labelEn: 'Address', type: 'textarea', required: true },
|
||||
{ key: 'effectiveDate', labelDe: 'Wirksam ab', labelEn: 'Effective date', type: 'date', required: true },
|
||||
{ key: 'soleRepresentation', labelDe: 'Einzelvertretungsbefugnis', labelEn: 'Sole representation', type: 'text' },
|
||||
{ key: 'selfDealingExempt', labelDe: 'Befreiung von § 181 BGB', labelEn: 'Exempt from § 181 BGB', type: 'text' },
|
||||
],
|
||||
render: ({ values, company, members, createdBy, date }) => {
|
||||
const v = schema.parse(values)
|
||||
const body = `
|
||||
<h1>Gesellschafterbeschluss</h1>
|
||||
<div class="meta">
|
||||
${escapeHtml(company.name)}${company.registryNo ? `, ${escapeHtml(company.registryNo)}` : ''}<br/>
|
||||
${company.address ? escapeHtml(company.address) + '<br/>' : ''}
|
||||
Beschlussdatum: ${escapeHtml(formatDateDe(date))}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gegenstand des Beschlusses</h2>
|
||||
<p>Bestellung von <strong>${escapeHtml(v.appointeeName)}</strong>,
|
||||
${escapeHtml(v.appointeeAddress)}, zum Geschäftsführer der Gesellschaft
|
||||
mit Wirkung zum <strong>${escapeHtml(formatDateDe(v.effectiveDate))}</strong>.</p>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Beschluss</h2>
|
||||
<ul>
|
||||
<li>${escapeHtml(v.appointeeName)} wird zum Geschäftsführer bestellt.</li>
|
||||
${v.soleRepresentation ? '<li>Einzelvertretungsbefugnis wird erteilt.</li>' : ''}
|
||||
${v.selfDealingExempt ? '<li>Befreiung von den Beschränkungen des § 181 BGB wird erteilt.</li>' : ''}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gesellschafter</h2>
|
||||
${renderMembers(members)}
|
||||
</div>
|
||||
<div class="section signature">
|
||||
<div class="line">${escapeHtml(createdBy.displayName)}<br/><small>${escapeHtml(createdBy.email)}</small></div>
|
||||
<div class="line">Ort, Datum</div>
|
||||
</div>
|
||||
`
|
||||
return baseDocument('Beschluss – Bestellung Geschäftsführer', body)
|
||||
},
|
||||
}
|
||||
66
packages/functions/src/templates/md-dismissal.ts
Normal file
66
packages/functions/src/templates/md-dismissal.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { z } from 'zod'
|
||||
import type { ResolutionTemplate } from './index.js'
|
||||
import {
|
||||
baseDocument,
|
||||
escapeHtml,
|
||||
formatDateDe,
|
||||
renderMembers,
|
||||
} from './shared.js'
|
||||
|
||||
const schema = z.object({
|
||||
dismisseeName: z.string().min(2),
|
||||
effectiveDate: z.string().min(8),
|
||||
reason: z.string().optional(),
|
||||
releaseFromContract: z.coerce.boolean().optional(),
|
||||
})
|
||||
|
||||
export const mdDismissal: ResolutionTemplate = {
|
||||
key: 'md_dismissal',
|
||||
titleDe: 'Beschluss zur Abberufung eines Geschäftsführers',
|
||||
titleEn: 'Managing director dismissal',
|
||||
descriptionDe:
|
||||
'Gesellschafterbeschluss zur Abberufung einer Person aus der Geschäftsführung gemäß § 38 GmbHG.',
|
||||
schema,
|
||||
fields: [
|
||||
{ key: 'dismisseeName', labelDe: 'Name des Geschäftsführers', labelEn: 'Director name', type: 'text', required: true },
|
||||
{ key: 'effectiveDate', labelDe: 'Wirksam ab', labelEn: 'Effective date', type: 'date', required: true },
|
||||
{ key: 'reason', labelDe: 'Begründung', labelEn: 'Reason', type: 'textarea' },
|
||||
{ key: 'releaseFromContract', labelDe: 'Anstellungsvertrag wird aufgehoben', labelEn: 'Employment contract terminated', type: 'text' },
|
||||
],
|
||||
render: ({ values, company, members, createdBy, date }) => {
|
||||
const v = schema.parse(values)
|
||||
const body = `
|
||||
<h1>Gesellschafterbeschluss</h1>
|
||||
<div class="meta">
|
||||
${escapeHtml(company.name)}${company.registryNo ? `, ${escapeHtml(company.registryNo)}` : ''}<br/>
|
||||
${company.address ? escapeHtml(company.address) + '<br/>' : ''}
|
||||
Beschlussdatum: ${escapeHtml(formatDateDe(date))}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gegenstand des Beschlusses</h2>
|
||||
<p>Abberufung von <strong>${escapeHtml(v.dismisseeName)}</strong> als
|
||||
Geschäftsführer der Gesellschaft mit Wirkung zum
|
||||
<strong>${escapeHtml(formatDateDe(v.effectiveDate))}</strong> gemäß § 38 GmbHG.</p>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Beschluss</h2>
|
||||
<ul>
|
||||
<li>${escapeHtml(v.dismisseeName)} wird mit Wirkung zum ${escapeHtml(formatDateDe(v.effectiveDate))} aus der Geschäftsführung abberufen.</li>
|
||||
${v.releaseFromContract ? '<li>Der zugrundeliegende Anstellungsvertrag wird einvernehmlich aufgehoben.</li>' : ''}
|
||||
<li>Der Geschäftsführer wird angewiesen, alle Geschäftsunterlagen, Schlüssel und Vermögensgegenstände der Gesellschaft unverzüglich zurückzugeben.</li>
|
||||
<li>Die Anmeldung zum Handelsregister wird veranlasst.</li>
|
||||
</ul>
|
||||
${v.reason ? `<p><strong>Begründung:</strong> ${escapeHtml(v.reason)}</p>` : ''}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gesellschafter</h2>
|
||||
${renderMembers(members)}
|
||||
</div>
|
||||
<div class="section signature">
|
||||
<div class="line">${escapeHtml(createdBy.displayName)}<br/><small>${escapeHtml(createdBy.email)}</small></div>
|
||||
<div class="line">Ort, Datum</div>
|
||||
</div>
|
||||
`
|
||||
return baseDocument('Beschluss – Abberufung Geschäftsführer', body)
|
||||
},
|
||||
}
|
||||
71
packages/functions/src/templates/profit-distribution.ts
Normal file
71
packages/functions/src/templates/profit-distribution.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { z } from 'zod'
|
||||
import type { ResolutionTemplate } from './index.js'
|
||||
import {
|
||||
baseDocument,
|
||||
escapeHtml,
|
||||
formatDateDe,
|
||||
formatEur,
|
||||
renderMembers,
|
||||
} from './shared.js'
|
||||
|
||||
const schema = z.object({
|
||||
fiscalYear: z.string().min(4),
|
||||
distributableProfit: z.coerce.number().nonnegative(),
|
||||
distributedAmount: z.coerce.number().nonnegative(),
|
||||
retainedAmount: z.coerce.number().nonnegative(),
|
||||
paymentDate: z.string().min(8),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
|
||||
export const profitDistribution: ResolutionTemplate = {
|
||||
key: 'profit_distribution',
|
||||
titleDe: 'Beschluss zur Gewinnverwendung',
|
||||
titleEn: 'Profit distribution',
|
||||
descriptionDe:
|
||||
'Gesellschafterbeschluss über die Verwendung des Jahresergebnisses gemäß § 29 GmbHG.',
|
||||
schema,
|
||||
fields: [
|
||||
{ key: 'fiscalYear', labelDe: 'Geschäftsjahr', labelEn: 'Fiscal year', type: 'text', required: true },
|
||||
{ key: 'distributableProfit', labelDe: 'Bilanzgewinn (EUR)', labelEn: 'Distributable profit (EUR)', type: 'currency', required: true },
|
||||
{ key: 'distributedAmount', labelDe: 'Auszuschüttender Betrag (EUR)', labelEn: 'Amount to distribute (EUR)', type: 'currency', required: true },
|
||||
{ key: 'retainedAmount', labelDe: 'Gewinnvortrag (EUR)', labelEn: 'Retained amount (EUR)', type: 'currency', required: true },
|
||||
{ key: 'paymentDate', labelDe: 'Auszahlungsdatum', labelEn: 'Payment date', type: 'date', required: true },
|
||||
{ key: 'notes', labelDe: 'Anmerkungen', labelEn: 'Notes', type: 'textarea' },
|
||||
],
|
||||
render: ({ values, company, members, createdBy, date }) => {
|
||||
const v = schema.parse(values)
|
||||
const body = `
|
||||
<h1>Gesellschafterbeschluss</h1>
|
||||
<div class="meta">
|
||||
${escapeHtml(company.name)}${company.registryNo ? `, ${escapeHtml(company.registryNo)}` : ''}<br/>
|
||||
${company.address ? escapeHtml(company.address) + '<br/>' : ''}
|
||||
Beschlussdatum: ${escapeHtml(formatDateDe(date))}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gegenstand des Beschlusses</h2>
|
||||
<p>Verwendung des Jahresergebnisses für das Geschäftsjahr
|
||||
<strong>${escapeHtml(v.fiscalYear)}</strong> gemäß § 29 GmbHG.</p>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Beschluss</h2>
|
||||
<ul>
|
||||
<li>Bilanzgewinn: <strong>${escapeHtml(formatEur(v.distributableProfit))}</strong></li>
|
||||
<li>Auszuschüttender Betrag: <strong>${escapeHtml(formatEur(v.distributedAmount))}</strong></li>
|
||||
<li>Gewinnvortrag: <strong>${escapeHtml(formatEur(v.retainedAmount))}</strong></li>
|
||||
<li>Auszahlung am: <strong>${escapeHtml(formatDateDe(v.paymentDate))}</strong></li>
|
||||
</ul>
|
||||
<p>Die Ausschüttung erfolgt im Verhältnis der Geschäftsanteile.</p>
|
||||
${v.notes ? `<p><em>${escapeHtml(v.notes)}</em></p>` : ''}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gesellschafter</h2>
|
||||
${renderMembers(members)}
|
||||
</div>
|
||||
<div class="section signature">
|
||||
<div class="line">${escapeHtml(createdBy.displayName)}<br/><small>${escapeHtml(createdBy.email)}</small></div>
|
||||
<div class="line">Ort, Datum</div>
|
||||
</div>
|
||||
`
|
||||
return baseDocument('Beschluss – Gewinnverwendung', body)
|
||||
},
|
||||
}
|
||||
83
packages/functions/src/templates/share-transfer-approval.ts
Normal file
83
packages/functions/src/templates/share-transfer-approval.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { z } from 'zod'
|
||||
import type { ResolutionTemplate } from './index.js'
|
||||
import {
|
||||
baseDocument,
|
||||
escapeHtml,
|
||||
formatDateDe,
|
||||
formatEur,
|
||||
renderMembers,
|
||||
} from './shared.js'
|
||||
|
||||
const schema = z.object({
|
||||
sellerName: z.string().min(2),
|
||||
buyerName: z.string().min(2),
|
||||
buyerAddress: z.string().min(2),
|
||||
shareAmount: z.coerce.number().positive(),
|
||||
totalSharesAfter: z.coerce.number().nonnegative(),
|
||||
purchasePrice: z.coerce.number().nonnegative(),
|
||||
effectiveDate: z.string().min(8),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
|
||||
export const shareTransferApproval: ResolutionTemplate = {
|
||||
key: 'share_transfer_approval',
|
||||
titleDe: 'Genehmigung der Anteilsübertragung',
|
||||
titleEn: 'Share transfer approval',
|
||||
descriptionDe:
|
||||
'Gesellschafterbeschluss zur Genehmigung der Übertragung von Geschäftsanteilen gemäß § 15 GmbHG i.V.m. der Satzung. Die Übertragung selbst ist beurkundungspflichtig.',
|
||||
schema,
|
||||
fields: [
|
||||
{ key: 'sellerName', labelDe: 'Veräußerer', labelEn: 'Seller', type: 'text', required: true },
|
||||
{ key: 'buyerName', labelDe: 'Erwerber', labelEn: 'Buyer', type: 'text', required: true },
|
||||
{ key: 'buyerAddress', labelDe: 'Anschrift des Erwerbers', labelEn: 'Buyer address', type: 'textarea', required: true },
|
||||
{ key: 'shareAmount', labelDe: 'Übertragener Anteil (EUR Nennbetrag)', labelEn: 'Transferred share (EUR nominal)', type: 'currency', required: true },
|
||||
{ key: 'totalSharesAfter', labelDe: 'Anteile des Erwerbers nach Übertragung (EUR Nennbetrag)', labelEn: 'Buyer total shares after transfer (EUR nominal)', type: 'currency', required: true },
|
||||
{ key: 'purchasePrice', labelDe: 'Kaufpreis (EUR)', labelEn: 'Purchase price (EUR)', type: 'currency', required: true },
|
||||
{ key: 'effectiveDate', labelDe: 'Wirksam ab', labelEn: 'Effective date', type: 'date', required: true },
|
||||
{ key: 'notes', labelDe: 'Anmerkungen', labelEn: 'Notes', type: 'textarea' },
|
||||
],
|
||||
render: ({ values, company, members, createdBy, date }) => {
|
||||
const v = schema.parse(values)
|
||||
const body = `
|
||||
<h1>Gesellschafterbeschluss</h1>
|
||||
<div class="meta">
|
||||
${escapeHtml(company.name)}${company.registryNo ? `, ${escapeHtml(company.registryNo)}` : ''}<br/>
|
||||
${company.address ? escapeHtml(company.address) + '<br/>' : ''}
|
||||
Beschlussdatum: ${escapeHtml(formatDateDe(date))}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gegenstand des Beschlusses</h2>
|
||||
<p>Genehmigung der Übertragung eines Geschäftsanteils im Nennbetrag von
|
||||
<strong>${escapeHtml(formatEur(v.shareAmount))}</strong> von
|
||||
<strong>${escapeHtml(v.sellerName)}</strong> auf
|
||||
<strong>${escapeHtml(v.buyerName)}</strong> gemäß § 15 GmbHG
|
||||
in Verbindung mit der Satzung der Gesellschaft.</p>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Beschluss</h2>
|
||||
<ul>
|
||||
<li>Veräußerer: <strong>${escapeHtml(v.sellerName)}</strong></li>
|
||||
<li>Erwerber: <strong>${escapeHtml(v.buyerName)}</strong>, ${escapeHtml(v.buyerAddress)}</li>
|
||||
<li>Übertragener Anteil (Nennbetrag): <strong>${escapeHtml(formatEur(v.shareAmount))}</strong></li>
|
||||
<li>Kaufpreis: <strong>${escapeHtml(formatEur(v.purchasePrice))}</strong></li>
|
||||
<li>Anteile des Erwerbers nach Übertragung (Nennbetrag): <strong>${escapeHtml(formatEur(v.totalSharesAfter))}</strong></li>
|
||||
<li>Wirksam ab: <strong>${escapeHtml(formatDateDe(v.effectiveDate))}</strong></li>
|
||||
</ul>
|
||||
<p>Die Wirksamkeit der Übertragung setzt eine notarielle Beurkundung des
|
||||
Übertragungsvertrages gemäß § 15 Abs. 3 GmbHG voraus. Die
|
||||
Geschäftsführung wird angewiesen, eine aktualisierte Gesellschafterliste
|
||||
beim Handelsregister einzureichen.</p>
|
||||
${v.notes ? `<p><em>${escapeHtml(v.notes)}</em></p>` : ''}
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Gesellschafter</h2>
|
||||
${renderMembers(members)}
|
||||
</div>
|
||||
<div class="section signature">
|
||||
<div class="line">${escapeHtml(createdBy.displayName)}<br/><small>${escapeHtml(createdBy.email)}</small></div>
|
||||
<div class="line">Ort, Datum</div>
|
||||
</div>
|
||||
`
|
||||
return baseDocument('Beschluss – Anteilsübertragung', body)
|
||||
},
|
||||
}
|
||||
64
packages/functions/src/templates/shared.ts
Normal file
64
packages/functions/src/templates/shared.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export const escapeHtml = (s: unknown): string =>
|
||||
String(s ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
|
||||
export const formatEur = (n: unknown): string => {
|
||||
const num = typeof n === 'number' ? n : Number(n ?? 0)
|
||||
if (!Number.isFinite(num)) return '—'
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
}).format(num)
|
||||
}
|
||||
|
||||
export const formatDateDe = (d: string | Date): string => {
|
||||
const date = typeof d === 'string' ? new Date(d) : d
|
||||
return new Intl.DateTimeFormat('de-DE', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
export const baseDocument = (title: string, body: string): string => `<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${escapeHtml(title)}</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 24mm 20mm; }
|
||||
body { font-family: 'Helvetica Neue', Arial, sans-serif; font-size: 11pt; color: #111; line-height: 1.5; }
|
||||
h1 { font-size: 16pt; margin: 0 0 4mm 0; }
|
||||
h2 { font-size: 13pt; margin: 8mm 0 2mm 0; }
|
||||
.meta { color: #555; font-size: 10pt; margin-bottom: 8mm; }
|
||||
.section { margin: 6mm 0; }
|
||||
ul { margin: 2mm 0; padding-left: 6mm; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
td, th { border: 1px solid #ddd; padding: 2mm 3mm; text-align: left; vertical-align: top; }
|
||||
.signature { margin-top: 16mm; display: flex; gap: 12mm; }
|
||||
.signature .line { flex: 1; border-top: 1px solid #333; padding-top: 2mm; font-size: 10pt; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${body}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
export const renderMembers = (
|
||||
members: Array<{ displayName: string; role: string; shares: number | null }>,
|
||||
): string => {
|
||||
const rows = members
|
||||
.map(
|
||||
(m) =>
|
||||
`<tr><td>${escapeHtml(m.displayName)}</td><td>${escapeHtml(m.role)}</td><td>${m.shares ?? '—'}</td></tr>`,
|
||||
)
|
||||
.join('')
|
||||
return `<table>
|
||||
<thead><tr><th>Name</th><th>Rolle</th><th>Anteile</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`
|
||||
}
|
||||
Reference in New Issue
Block a user