@kirjapro/mcp-server 0.5.11 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +2 -1
- package/dist/accounts.d.ts +40 -0
- package/dist/accounts.js +45 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.js +245 -0
- package/dist/compliance/rates.d.ts +53 -0
- package/dist/compliance/rates.js +57 -0
- package/dist/database.types.d.ts +6525 -0
- package/dist/database.types.js +20 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +248 -105
- package/dist/lib/incomeStatementSynthesis.d.ts +47 -0
- package/dist/lib/incomeStatementSynthesis.js +140 -0
- package/dist/resources/company.d.ts +2 -0
- package/dist/resources/company.js +75 -0
- package/dist/resources/compliance.d.ts +2 -0
- package/dist/resources/compliance.js +338 -0
- package/dist/resources/index.d.ts +2 -0
- package/dist/resources/index.js +10 -0
- package/dist/resources/schema.d.ts +2 -0
- package/dist/resources/schema.js +63 -0
- package/dist/resources/security.d.ts +2 -0
- package/dist/resources/security.js +33 -0
- package/dist/schemas.d.ts +12 -0
- package/dist/schemas.js +12 -0
- package/dist/tools/accounts.d.ts +2 -0
- package/dist/tools/accounts.js +48 -0
- package/dist/tools/banking.d.ts +2 -0
- package/dist/tools/banking.js +196 -0
- package/dist/tools/bookings.d.ts +2 -0
- package/dist/tools/bookings.js +294 -0
- package/dist/tools/customers.d.ts +2 -0
- package/dist/tools/customers.js +320 -0
- package/dist/tools/financialNotes.d.ts +2 -0
- package/dist/tools/financialNotes.js +255 -0
- package/dist/tools/fingerprints.d.ts +6 -0
- package/dist/tools/fingerprints.js +28 -0
- package/dist/tools/fiscal-years.d.ts +2 -0
- package/dist/tools/fiscal-years.js +35 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +105 -0
- package/dist/tools/invoices.d.ts +2 -0
- package/dist/tools/invoices.js +489 -0
- package/dist/tools/offers.d.ts +2 -0
- package/dist/tools/offers.js +443 -0
- package/dist/tools/received-invoices.d.ts +2 -0
- package/dist/tools/received-invoices.js +134 -0
- package/dist/tools/recurring-invoices.d.ts +2 -0
- package/dist/tools/recurring-invoices.js +348 -0
- package/dist/tools/reports.d.ts +2 -0
- package/dist/tools/reports.js +869 -0
- package/dist/tools/templates.d.ts +89 -0
- package/dist/tools/templates.js +636 -0
- package/dist/tools/transactions.d.ts +2 -0
- package/dist/tools/transactions.js +597 -0
- package/dist/tools/validation.d.ts +26 -0
- package/dist/tools/validation.js +127 -0
- package/dist/types.d.ts +388 -0
- package/dist/types.js +34 -0
- package/dist/utils.d.ts +276 -0
- package/dist/utils.js +637 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +5 -2
- package/server.json +5 -1
- package/tool-fingerprints.json +10 -10
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getSupabase, getCompanyId } from '../auth.js';
|
|
3
|
+
import { clampPagination, paginate, toResult, toErrorContent, toDryRunResponse, sanitizeDbError, invokeEdgeFunction, tagCustomer, withIdempotency, IDEMPOTENCY_KEY_SCHEMA, ISO_DATE_SCHEMA, DRY_RUN_SCHEMA, READ_ONLY_ANNOTATIONS, WRITE_ANNOTATIONS, DESTRUCTIVE_ANNOTATIONS } from '../utils.js';
|
|
4
|
+
import { PAGINATED_OUTPUT_SHAPE } from '../schemas.js';
|
|
5
|
+
const RECURRING_INVOICE_DETAIL_OUTPUT_SHAPE = {
|
|
6
|
+
id: z.string(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
frequency: z.string(),
|
|
9
|
+
next_date: z.string().nullable(),
|
|
10
|
+
is_active: z.boolean(),
|
|
11
|
+
template_data: z.record(z.string(), z.unknown()).nullable(),
|
|
12
|
+
customer: z.record(z.string(), z.unknown()).nullable(),
|
|
13
|
+
recent_logs: z.array(z.record(z.string(), z.unknown())),
|
|
14
|
+
};
|
|
15
|
+
export function registerRecurringInvoiceTools(server) {
|
|
16
|
+
// ---- LIST ----
|
|
17
|
+
server.registerTool('kirjapro_list_recurring_invoices', {
|
|
18
|
+
description: 'List recurring invoice templates with pagination. Shows frequency, next generation date, active status, and customer info.',
|
|
19
|
+
inputSchema: {
|
|
20
|
+
limit: z.number().min(1).max(200).default(50).describe('Max results (default 50)'),
|
|
21
|
+
offset: z.number().min(0).default(0).describe('Skip first N results'),
|
|
22
|
+
activeOnly: z.boolean().default(true).describe('Only show active templates (default true)'),
|
|
23
|
+
},
|
|
24
|
+
outputSchema: PAGINATED_OUTPUT_SHAPE,
|
|
25
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
26
|
+
}, async (params) => {
|
|
27
|
+
const supabase = getSupabase();
|
|
28
|
+
const companyId = getCompanyId();
|
|
29
|
+
const { limit, offset } = clampPagination(params.limit, params.offset);
|
|
30
|
+
let query = supabase
|
|
31
|
+
.from('recurring_invoices')
|
|
32
|
+
.select('id, name, frequency, next_date, end_date, is_active, auto_send, last_generated_at, times_generated, created_at, customer:customers(id, name)', { count: 'exact' })
|
|
33
|
+
.eq('company_id', companyId)
|
|
34
|
+
.order('next_date');
|
|
35
|
+
if (params.activeOnly) {
|
|
36
|
+
query = query.eq('is_active', true);
|
|
37
|
+
}
|
|
38
|
+
query = query.range(offset, offset + limit - 1);
|
|
39
|
+
const { data, count, error } = await query;
|
|
40
|
+
if (error) {
|
|
41
|
+
return toErrorContent(sanitizeDbError("Failed to list recurring invoices", error));
|
|
42
|
+
}
|
|
43
|
+
const tagged = (data || []).map((row) => ({
|
|
44
|
+
...row,
|
|
45
|
+
customer: tagCustomer(row.customer),
|
|
46
|
+
}));
|
|
47
|
+
return toResult(paginate(tagged, count || 0, offset, limit));
|
|
48
|
+
});
|
|
49
|
+
// ---- GET ----
|
|
50
|
+
server.registerTool('kirjapro_get_recurring_invoice', {
|
|
51
|
+
description: 'Get a single recurring invoice template with full details including template data (lines, notes, payment terms) and generation history.',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
recurringInvoiceId: z.string().uuid().describe('Recurring invoice UUID'),
|
|
54
|
+
},
|
|
55
|
+
outputSchema: RECURRING_INVOICE_DETAIL_OUTPUT_SHAPE,
|
|
56
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
57
|
+
}, async (params) => {
|
|
58
|
+
const supabase = getSupabase();
|
|
59
|
+
const companyId = getCompanyId();
|
|
60
|
+
// Explicit column allowlist — no select('*'). Adding a new column to
|
|
61
|
+
// recurring_invoices must be a conscious decision here, since template_data
|
|
62
|
+
// is a free-form JSONB blob the agent could act on.
|
|
63
|
+
const { data, error } = await supabase
|
|
64
|
+
.from('recurring_invoices')
|
|
65
|
+
.select('id, company_id, customer_id, name, frequency, next_date, end_date, is_active, auto_send, template_data, last_generated_at, times_generated, created_at, updated_at, customer:customers(id, name, business_id, email)')
|
|
66
|
+
.eq('id', params.recurringInvoiceId)
|
|
67
|
+
.eq('company_id', companyId)
|
|
68
|
+
.single();
|
|
69
|
+
if (error || !data) {
|
|
70
|
+
return toErrorContent(`Recurring invoice ${params.recurringInvoiceId} not found. Use kirjapro_list_recurring_invoices to find IDs.`);
|
|
71
|
+
}
|
|
72
|
+
// Fetch recent generation logs
|
|
73
|
+
const { data: logs } = await supabase
|
|
74
|
+
.from('recurring_invoice_logs')
|
|
75
|
+
.select('id, status, scheduled_date, invoice_number, error_message, created_at')
|
|
76
|
+
.eq('recurring_invoice_id', params.recurringInvoiceId)
|
|
77
|
+
.eq('company_id', companyId)
|
|
78
|
+
.order('created_at', { ascending: false })
|
|
79
|
+
.limit(10);
|
|
80
|
+
return toResult({
|
|
81
|
+
...data,
|
|
82
|
+
customer: tagCustomer(data
|
|
83
|
+
.customer),
|
|
84
|
+
recent_logs: logs || [],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
// ---- CREATE ----
|
|
88
|
+
server.registerTool('kirjapro_create_recurring_invoice', {
|
|
89
|
+
description: 'Create a new recurring invoice template. Specify customer, frequency, start date, and invoice line items. Invoices will be generated automatically on the schedule.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
customerId: z.string().uuid().describe('Customer UUID'),
|
|
92
|
+
name: z.string().min(1).max(200).describe('Template name (e.g. "Monthly hosting fee")'),
|
|
93
|
+
frequency: z.enum(['weekly', 'monthly', 'quarterly', 'yearly']).describe('Generation frequency'),
|
|
94
|
+
nextDate: ISO_DATE_SCHEMA.describe('First generation date (YYYY-MM-DD)'),
|
|
95
|
+
endDate: ISO_DATE_SCHEMA.optional().describe('Stop generating after this date'),
|
|
96
|
+
autoSend: z.boolean().default(false).describe('Auto-send generated invoices (default false)'),
|
|
97
|
+
lines: z.array(z.object({
|
|
98
|
+
description: z.string().describe('Line item description'),
|
|
99
|
+
quantity: z.number().positive().describe('Quantity'),
|
|
100
|
+
unit: z.string().default('kpl').describe('Unit (default "kpl")'),
|
|
101
|
+
unitPrice: z
|
|
102
|
+
.number()
|
|
103
|
+
.finite()
|
|
104
|
+
.min(-1_000_000_000)
|
|
105
|
+
.max(1_000_000_000)
|
|
106
|
+
.describe('Unit price in EUR (excl. VAT). Can be negative for credit lines.'),
|
|
107
|
+
vatRate: z.enum(['25.5', '13.5', '10', '0']).describe('VAT rate'),
|
|
108
|
+
})).min(1).describe('Invoice line items'),
|
|
109
|
+
notes: z.string().optional().describe('Invoice notes'),
|
|
110
|
+
paymentTermDays: z.number().min(0).max(365).default(14).describe('Payment term in days'),
|
|
111
|
+
idempotencyKey: IDEMPOTENCY_KEY_SCHEMA,
|
|
112
|
+
},
|
|
113
|
+
annotations: WRITE_ANNOTATIONS,
|
|
114
|
+
}, async (params) => {
|
|
115
|
+
const supabase = getSupabase();
|
|
116
|
+
const companyId = getCompanyId();
|
|
117
|
+
return withIdempotency(supabase, companyId, 'kirjapro_create_recurring_invoice', params.idempotencyKey, params, async () => {
|
|
118
|
+
// Verify customer exists
|
|
119
|
+
const { data: customer, error: custError } = await supabase
|
|
120
|
+
.from('customers')
|
|
121
|
+
.select('id, name')
|
|
122
|
+
.eq('id', params.customerId)
|
|
123
|
+
.eq('company_id', companyId)
|
|
124
|
+
.maybeSingle();
|
|
125
|
+
if (custError || !customer) {
|
|
126
|
+
return toErrorContent(`Customer ${params.customerId} not found. Use kirjapro_list_customers to find customer IDs.`);
|
|
127
|
+
}
|
|
128
|
+
const templateData = {
|
|
129
|
+
lines: params.lines.map((l, i) => ({
|
|
130
|
+
line_number: i + 1,
|
|
131
|
+
description: l.description,
|
|
132
|
+
quantity: l.quantity,
|
|
133
|
+
unit: l.unit,
|
|
134
|
+
unit_price: l.unitPrice,
|
|
135
|
+
vat_rate: parseFloat(l.vatRate),
|
|
136
|
+
})),
|
|
137
|
+
notes: params.notes || null,
|
|
138
|
+
payment_terms_days: params.paymentTermDays,
|
|
139
|
+
};
|
|
140
|
+
const { data, error } = await supabase
|
|
141
|
+
.from('recurring_invoices')
|
|
142
|
+
.insert({
|
|
143
|
+
company_id: companyId,
|
|
144
|
+
customer_id: params.customerId,
|
|
145
|
+
name: params.name,
|
|
146
|
+
frequency: params.frequency,
|
|
147
|
+
next_date: params.nextDate,
|
|
148
|
+
end_date: params.endDate || null,
|
|
149
|
+
is_active: true,
|
|
150
|
+
auto_send: params.autoSend,
|
|
151
|
+
template_data: templateData,
|
|
152
|
+
})
|
|
153
|
+
.select('id, name, frequency, next_date, end_date, is_active, auto_send, created_at, updated_at')
|
|
154
|
+
.single();
|
|
155
|
+
if (error) {
|
|
156
|
+
return toErrorContent(sanitizeDbError("Failed to create recurring invoice", error));
|
|
157
|
+
}
|
|
158
|
+
return toResult({
|
|
159
|
+
message: `Recurring invoice "${params.name}" created for ${customer.name} (${params.frequency}, next: ${params.nextDate})`,
|
|
160
|
+
recurring_invoice: data,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
// ---- UPDATE ----
|
|
165
|
+
server.registerTool('kirjapro_update_recurring_invoice', {
|
|
166
|
+
description: 'Update a recurring invoice template. Only provided fields are changed. Use this to pause/resume (isActive), change schedule, or update line items.',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
recurringInvoiceId: z.string().uuid().describe('Recurring invoice UUID'),
|
|
169
|
+
name: z.string().min(1).max(200).optional().describe('Template name'),
|
|
170
|
+
frequency: z.enum(['weekly', 'monthly', 'quarterly', 'yearly']).optional().describe('Generation frequency'),
|
|
171
|
+
nextDate: ISO_DATE_SCHEMA.optional().describe('Next generation date'),
|
|
172
|
+
endDate: ISO_DATE_SCHEMA.optional().nullable().describe('End date (null to remove)'),
|
|
173
|
+
isActive: z.boolean().optional().describe('Active status (false to pause)'),
|
|
174
|
+
autoSend: z.boolean().optional().describe('Auto-send generated invoices'),
|
|
175
|
+
lines: z.array(z.object({
|
|
176
|
+
description: z.string().describe('Line item description'),
|
|
177
|
+
quantity: z.number().positive().describe('Quantity'),
|
|
178
|
+
unit: z.string().default('kpl').describe('Unit'),
|
|
179
|
+
unitPrice: z
|
|
180
|
+
.number()
|
|
181
|
+
.finite()
|
|
182
|
+
.min(-1_000_000_000)
|
|
183
|
+
.max(1_000_000_000)
|
|
184
|
+
.describe('Unit price in EUR (excl. VAT). Can be negative for credit lines.'),
|
|
185
|
+
vatRate: z.enum(['25.5', '13.5', '10', '0']).describe('VAT rate'),
|
|
186
|
+
})).min(1).optional().describe('Replace all invoice lines'),
|
|
187
|
+
notes: z.string().optional().nullable().describe('Invoice notes (null to remove)'),
|
|
188
|
+
paymentTermDays: z.number().min(0).max(365).optional().describe('Payment term in days'),
|
|
189
|
+
idempotencyKey: IDEMPOTENCY_KEY_SCHEMA,
|
|
190
|
+
},
|
|
191
|
+
annotations: WRITE_ANNOTATIONS,
|
|
192
|
+
}, async (params) => {
|
|
193
|
+
const supabase = getSupabase();
|
|
194
|
+
const companyId = getCompanyId();
|
|
195
|
+
// Verify exists
|
|
196
|
+
const { data: existing, error: fetchError } = await supabase
|
|
197
|
+
.from('recurring_invoices')
|
|
198
|
+
.select('id, name, template_data, next_date, end_date')
|
|
199
|
+
.eq('id', params.recurringInvoiceId)
|
|
200
|
+
.eq('company_id', companyId)
|
|
201
|
+
.single();
|
|
202
|
+
if (fetchError || !existing) {
|
|
203
|
+
return toErrorContent(`Recurring invoice ${params.recurringInvoiceId} not found. Use kirjapro_list_recurring_invoices to find IDs.`);
|
|
204
|
+
}
|
|
205
|
+
// Cross-field date validation: the effective nextDate must not be after
|
|
206
|
+
// the effective endDate. Skipping this lets the caller create a schedule
|
|
207
|
+
// that will never fire (or immediately auto-deactivate on the next cron
|
|
208
|
+
// sweep), silently wasting an idempotency slot.
|
|
209
|
+
const effectiveNext = params.nextDate !== undefined ? params.nextDate : existing.next_date;
|
|
210
|
+
const effectiveEnd = params.endDate !== undefined ? params.endDate : existing.end_date;
|
|
211
|
+
if (effectiveEnd && effectiveNext && effectiveEnd < effectiveNext) {
|
|
212
|
+
return toErrorContent(`endDate (${effectiveEnd}) must not be earlier than nextDate (${effectiveNext}). ` +
|
|
213
|
+
`Either clear endDate (set to null) or advance nextDate first.`);
|
|
214
|
+
}
|
|
215
|
+
const updates = {};
|
|
216
|
+
if (params.name !== undefined)
|
|
217
|
+
updates.name = params.name;
|
|
218
|
+
if (params.frequency !== undefined)
|
|
219
|
+
updates.frequency = params.frequency;
|
|
220
|
+
if (params.nextDate !== undefined)
|
|
221
|
+
updates.next_date = params.nextDate;
|
|
222
|
+
if (params.endDate !== undefined)
|
|
223
|
+
updates.end_date = params.endDate;
|
|
224
|
+
if (params.isActive !== undefined)
|
|
225
|
+
updates.is_active = params.isActive;
|
|
226
|
+
if (params.autoSend !== undefined)
|
|
227
|
+
updates.auto_send = params.autoSend;
|
|
228
|
+
// Rebuild template_data if lines or notes/payment terms changed
|
|
229
|
+
if (params.lines !== undefined || params.notes !== undefined || params.paymentTermDays !== undefined) {
|
|
230
|
+
const currentTemplate = (existing.template_data || {});
|
|
231
|
+
const newTemplate = { ...currentTemplate };
|
|
232
|
+
if (params.lines !== undefined) {
|
|
233
|
+
newTemplate.lines = params.lines.map((l, i) => ({
|
|
234
|
+
line_number: i + 1,
|
|
235
|
+
description: l.description,
|
|
236
|
+
quantity: l.quantity,
|
|
237
|
+
unit: l.unit,
|
|
238
|
+
unit_price: l.unitPrice,
|
|
239
|
+
vat_rate: parseFloat(l.vatRate),
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
if (params.notes !== undefined)
|
|
243
|
+
newTemplate.notes = params.notes;
|
|
244
|
+
if (params.paymentTermDays !== undefined)
|
|
245
|
+
newTemplate.payment_terms_days = params.paymentTermDays;
|
|
246
|
+
updates.template_data = newTemplate;
|
|
247
|
+
}
|
|
248
|
+
if (Object.keys(updates).length === 0) {
|
|
249
|
+
return toErrorContent('No fields to update. Provide at least one field to change.');
|
|
250
|
+
}
|
|
251
|
+
return withIdempotency(supabase, companyId, 'kirjapro_update_recurring_invoice', params.idempotencyKey, params, async () => {
|
|
252
|
+
const { data, error } = await supabase
|
|
253
|
+
.from('recurring_invoices')
|
|
254
|
+
.update(updates)
|
|
255
|
+
.eq('id', params.recurringInvoiceId)
|
|
256
|
+
.eq('company_id', companyId)
|
|
257
|
+
.select('id, name, frequency, next_date, end_date, is_active, auto_send, created_at, updated_at')
|
|
258
|
+
.single();
|
|
259
|
+
if (error) {
|
|
260
|
+
return toErrorContent(sanitizeDbError("Failed to update recurring invoice", error));
|
|
261
|
+
}
|
|
262
|
+
return toResult({
|
|
263
|
+
message: `Recurring invoice "${data.name}" updated`,
|
|
264
|
+
recurring_invoice: data,
|
|
265
|
+
fields_changed: Object.keys(updates),
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
// ---- DELETE ----
|
|
270
|
+
server.registerTool('kirjapro_delete_recurring_invoice', {
|
|
271
|
+
description: 'Delete a recurring invoice template. Previously generated invoices are NOT affected.',
|
|
272
|
+
inputSchema: {
|
|
273
|
+
recurringInvoiceId: z.string().uuid().describe('Recurring invoice UUID'),
|
|
274
|
+
idempotencyKey: IDEMPOTENCY_KEY_SCHEMA,
|
|
275
|
+
dryRun: DRY_RUN_SCHEMA,
|
|
276
|
+
},
|
|
277
|
+
annotations: DESTRUCTIVE_ANNOTATIONS,
|
|
278
|
+
}, async (params) => {
|
|
279
|
+
const supabase = getSupabase();
|
|
280
|
+
const companyId = getCompanyId();
|
|
281
|
+
// Verify exists
|
|
282
|
+
const { data: existing, error: fetchError } = await supabase
|
|
283
|
+
.from('recurring_invoices')
|
|
284
|
+
.select('id, name')
|
|
285
|
+
.eq('id', params.recurringInvoiceId)
|
|
286
|
+
.eq('company_id', companyId)
|
|
287
|
+
.single();
|
|
288
|
+
if (fetchError || !existing) {
|
|
289
|
+
return toErrorContent(`Recurring invoice ${params.recurringInvoiceId} not found. Use kirjapro_list_recurring_invoices to find IDs.`);
|
|
290
|
+
}
|
|
291
|
+
if (params.dryRun) {
|
|
292
|
+
return toDryRunResponse('kirjapro_delete_recurring_invoice', {
|
|
293
|
+
action: 'delete_recurring_invoice',
|
|
294
|
+
name: existing.name,
|
|
295
|
+
note: 'Previously generated invoices will be unaffected.',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return withIdempotency(supabase, companyId, 'kirjapro_delete_recurring_invoice', params.idempotencyKey, params, async () => {
|
|
299
|
+
const { error } = await supabase
|
|
300
|
+
.from('recurring_invoices')
|
|
301
|
+
.delete()
|
|
302
|
+
.eq('id', params.recurringInvoiceId)
|
|
303
|
+
.eq('company_id', companyId);
|
|
304
|
+
if (error) {
|
|
305
|
+
return toErrorContent(sanitizeDbError("Failed to delete recurring invoice", error));
|
|
306
|
+
}
|
|
307
|
+
return toResult({
|
|
308
|
+
message: `Recurring invoice "${existing.name}" deleted. Previously generated invoices are unaffected.`,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
// ---- TRIGGER ----
|
|
313
|
+
server.registerTool('kirjapro_trigger_recurring_invoice', {
|
|
314
|
+
description: 'Manually trigger generation of a recurring invoice now, regardless of its next scheduled date. Returns the generated invoice.',
|
|
315
|
+
inputSchema: {
|
|
316
|
+
recurringInvoiceId: z.string().uuid().describe('Recurring invoice UUID'),
|
|
317
|
+
idempotencyKey: IDEMPOTENCY_KEY_SCHEMA,
|
|
318
|
+
},
|
|
319
|
+
annotations: WRITE_ANNOTATIONS,
|
|
320
|
+
}, async (params) => {
|
|
321
|
+
const supabase = getSupabase();
|
|
322
|
+
const companyId = getCompanyId();
|
|
323
|
+
return withIdempotency(supabase, companyId, 'kirjapro_trigger_recurring_invoice', params.idempotencyKey, params, async () => {
|
|
324
|
+
// Verify exists and is active
|
|
325
|
+
const { data: existing, error: fetchError } = await supabase
|
|
326
|
+
.from('recurring_invoices')
|
|
327
|
+
.select('id, name, is_active')
|
|
328
|
+
.eq('id', params.recurringInvoiceId)
|
|
329
|
+
.eq('company_id', companyId)
|
|
330
|
+
.single();
|
|
331
|
+
if (fetchError || !existing) {
|
|
332
|
+
return toErrorContent(`Recurring invoice ${params.recurringInvoiceId} not found. Use kirjapro_list_recurring_invoices to find IDs.`);
|
|
333
|
+
}
|
|
334
|
+
if (!existing.is_active) {
|
|
335
|
+
return toErrorContent(`Recurring invoice "${existing.name}" is paused. Activate it first with kirjapro_update_recurring_invoice.`);
|
|
336
|
+
}
|
|
337
|
+
const result = await invokeEdgeFunction(supabase, 'generate-recurring-invoices', { recurring_invoice_id: params.recurringInvoiceId, company_id: companyId });
|
|
338
|
+
if (result.error !== null) {
|
|
339
|
+
return toErrorContent(result.error);
|
|
340
|
+
}
|
|
341
|
+
return toResult({
|
|
342
|
+
message: `Invoice generated from "${existing.name}"`,
|
|
343
|
+
invoice_id: result.data.invoice_id,
|
|
344
|
+
invoice_number: result.data.invoice_number,
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
}
|