* fix(invoices): apply configured voucher series to payments + preview next voucher The booking engine resolves the series from default_voucher_series_per_source_type, but the global "Standardserie" dropdown wrote a separate field the engine ignored, and cash-method invoice payments (invoice_cash_payment) weren't exposed in settings — so configured series were silently dropped to "A". - Expose cash/private payment source types in the per-source-type form - Write the global default through to the map on save, keeping overrides - Resolve voucher-sequences/next by source_type (+date) to match the engine - Show the upcoming voucher (V2) in the payment dialog title - Share resolveInvoicePaymentSourceType so preview and booking can't drift Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(salary): keep AGI panel in sync with Skatteverket signing state The AGI panel mixed run-scoped generation state (agi_generated_at, agi_declarations) with period-scoped submission state (extension_data agi_submission_{period}), so the two could drift and present contradictory UI. Reconcile them: - Auto-detect a Mina Sidor BankID signature: while awaiting_signing, poll /agi/kvittenser on mount and on tab refocus so the panel flips to "signed" (hiding the signing actions) without a manual "Hamta kvittens" click. - Warn instead of offering to sign when the locked granskningsunderlag predates the run's latest AGI generation (draftIsStale) — avoids filing superseded figures. - Self-heal a stale "AGI-XML saknas" error once the run's AGI is (re)generated out-of-band (MCP/API/other tab). - Refetch the salary run on tab focus so agi_generated_at reflects out-of-band generation without a hard reload. - /agi/lasUpp now clears the cached agi_submission_{period} record, so unlocking drops the panel back to the pre-submission state instead of stranding it on a released "redo att signeras" draft. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: Implement VAT registration handling and invoice item line types - Added VAT registration check in commitCreateInvoice to set VAT rate to 0% for non-VAT registered companies. - Updated invoice creation logic to reflect 'exempt' VAT treatment and adjusted related fields accordingly. - Introduced support for free-text and blank spacer rows in invoice items by adding a new line_type field. - Enhanced invoice and credit note handling to accommodate new line types. - Added new localized messages for text rows in English and Swedish. - Created tests for salary run approval logic, ensuring bank details are validated correctly. - Implemented effective net payout calculation for salary runs, considering tax overrides. - Added SQL migrations to support new invoice item line types and accounting method awareness for linking invoices to vouchers. * feat(articles): artikelregister with revenue account + VAT rate per article Article register (non-inventory) with per-article VAT rate and optional BAS class-3 revenue-account override. Includes API routes, UI pages, MCP tools, pending-operation staging, and the activate-or-create account flow (ACCOUNTS_NOT_IN_CHART -> ActivateAccountsDialog, unknown numbers -> AddAccountDialog) reusing the journal entry UX. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): no-doc-required batch + bulk-missing endpoints Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(payments): supplier payment lines + cash-method invoice matching Shared payment-line proposal for supplier invoices, improved match-invoice/match-supplier-invoice flows (kontantmetoden-aware), and voucher-link support without requiring a 151x clearing entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): new journal entry dialog, SIE import tweaks, misc New journal entry dialog component, journal list/page updates, invoice editor updates, SIE import adjustments, transaction ingest and api-key tweaks, pr-agent workflow update. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): implement tax reduction features and localization updates * feat(tests): add VAT registration gate to pending operations commit tests --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
520 lines
16 KiB
TypeScript
520 lines
16 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import {
|
|
findMatchingVouchersForInvoice,
|
|
validateVoucherForInvoiceLink,
|
|
linkInvoiceToVoucher,
|
|
} from '../voucher-matching'
|
|
import {
|
|
makeInvoice,
|
|
createQueuedMockSupabase,
|
|
} from '@/tests/helpers'
|
|
import { eventBus } from '@/lib/events/bus'
|
|
|
|
// ============================================================
|
|
// validateVoucherForInvoiceLink — happy path + reject codes
|
|
// ============================================================
|
|
|
|
describe('validateVoucherForInvoiceLink', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
function setup(invoice = makeInvoice({ remaining_amount: 1000, total: 1000, currency: 'SEK' })) {
|
|
return invoice
|
|
}
|
|
|
|
it('rejects when the invoice has nothing remaining', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = setup(
|
|
makeInvoice({ remaining_amount: 0, paid_amount: 1000, total: 1000, currency: 'SEK' }),
|
|
)
|
|
enqueue({ data: null }) // unused — we short-circuit before querying
|
|
const result = await validateVoucherForInvoiceLink(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
'je-1',
|
|
)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.code).toBe('LINK_VOUCHER_INVOICE_FULLY_PAID')
|
|
})
|
|
|
|
it('rejects when the voucher is missing', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = setup()
|
|
enqueue({ data: { accounting_method: 'accrual' } }) // resolveAccountingMethod
|
|
enqueue({ data: null, error: null }) // journal_entries.maybeSingle → null
|
|
const result = await validateVoucherForInvoiceLink(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
'je-missing',
|
|
)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.code).toBe('LINK_VOUCHER_VOUCHER_NOT_FOUND')
|
|
})
|
|
|
|
it('rejects when the voucher is not posted', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = setup()
|
|
enqueue({ data: { accounting_method: 'accrual' } }) // resolveAccountingMethod
|
|
enqueue({
|
|
data: {
|
|
id: 'je-1',
|
|
voucher_series: 'A',
|
|
voucher_number: 5,
|
|
entry_date: '2026-05-01',
|
|
description: '',
|
|
status: 'draft',
|
|
source_type: 'manual',
|
|
fiscal_period_id: 'fp-1',
|
|
company_id: 'company-1',
|
|
},
|
|
})
|
|
const result = await validateVoucherForInvoiceLink(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
'je-1',
|
|
)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.code).toBe('LINK_VOUCHER_NOT_POSTED')
|
|
})
|
|
|
|
it('rejects when the voucher has no AR credit (accrual)', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = setup()
|
|
enqueue({ data: { accounting_method: 'accrual' } }) // resolveAccountingMethod
|
|
enqueue({
|
|
data: {
|
|
id: 'je-1',
|
|
voucher_series: 'A',
|
|
voucher_number: 5,
|
|
entry_date: '2026-05-01',
|
|
description: '',
|
|
status: 'posted',
|
|
source_type: 'manual',
|
|
fiscal_period_id: 'fp-1',
|
|
company_id: 'company-1',
|
|
},
|
|
})
|
|
enqueue({
|
|
data: [
|
|
{ account_number: '1930', debit_amount: 1000, credit_amount: 0, currency: 'SEK' },
|
|
{ account_number: '3001', debit_amount: 0, credit_amount: 1000, currency: 'SEK' },
|
|
],
|
|
})
|
|
const result = await validateVoucherForInvoiceLink(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
'je-1',
|
|
)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.code).toBe('LINK_VOUCHER_NO_AR_CREDIT')
|
|
})
|
|
|
|
it('cash method: accepts the same 1930-debit voucher that accrual rejects', async () => {
|
|
// Kontantmetoden books debit 19xx / credit 30xx and never touches 1510, so
|
|
// the matcher keys on the bank/cash debit instead of an AR credit.
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = setup()
|
|
enqueue({ data: { accounting_method: 'cash' } }) // resolveAccountingMethod
|
|
enqueue({
|
|
data: {
|
|
id: 'je-1',
|
|
voucher_series: 'A',
|
|
voucher_number: 5,
|
|
entry_date: '2026-05-01',
|
|
description: '',
|
|
status: 'posted',
|
|
source_type: 'manual',
|
|
fiscal_period_id: 'fp-1',
|
|
company_id: 'company-1',
|
|
},
|
|
})
|
|
enqueue({
|
|
data: [
|
|
{ account_number: '1930', debit_amount: 1000, credit_amount: 0, currency: 'SEK' },
|
|
{ account_number: '3001', debit_amount: 0, credit_amount: 1000, currency: 'SEK' },
|
|
],
|
|
})
|
|
enqueue({ data: [] }) // invoice_payments already-linked lookup
|
|
const result = await validateVoucherForInvoiceLink(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
'je-1',
|
|
)
|
|
expect(result.ok).toBe(true)
|
|
if (result.ok) {
|
|
expect(result.arCreditAmount).toBe(1000)
|
|
expect(result.paymentAmount).toBe(1000)
|
|
expect(result.isFullyPaid).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('cash method: rejects when the voucher has no bank/cash debit', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = setup()
|
|
enqueue({ data: { accounting_method: 'cash' } }) // resolveAccountingMethod
|
|
enqueue({
|
|
data: {
|
|
id: 'je-1',
|
|
voucher_series: 'A',
|
|
voucher_number: 5,
|
|
entry_date: '2026-05-01',
|
|
description: '',
|
|
status: 'posted',
|
|
source_type: 'manual',
|
|
fiscal_period_id: 'fp-1',
|
|
company_id: 'company-1',
|
|
},
|
|
})
|
|
enqueue({
|
|
data: [
|
|
// An AR-clearing voucher (1510 credit) — valid on accrual, but on cash
|
|
// there is no 19xx debit so it must not match.
|
|
{ account_number: '1510', debit_amount: 0, credit_amount: 1000, currency: 'SEK' },
|
|
{ account_number: '3001', debit_amount: 1000, credit_amount: 0, currency: 'SEK' },
|
|
],
|
|
})
|
|
const result = await validateVoucherForInvoiceLink(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
'je-1',
|
|
)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.code).toBe('LINK_VOUCHER_NO_AR_CREDIT')
|
|
})
|
|
|
|
it('rejects when the voucher amount exceeds the remaining', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = setup()
|
|
enqueue({ data: { accounting_method: 'accrual' } }) // resolveAccountingMethod
|
|
enqueue({
|
|
data: {
|
|
id: 'je-1',
|
|
voucher_series: 'A',
|
|
voucher_number: 5,
|
|
entry_date: '2026-05-01',
|
|
description: '',
|
|
status: 'posted',
|
|
source_type: 'manual',
|
|
fiscal_period_id: 'fp-1',
|
|
company_id: 'company-1',
|
|
},
|
|
})
|
|
enqueue({
|
|
data: [
|
|
{ account_number: '1930', debit_amount: 5000, credit_amount: 0, currency: 'SEK' },
|
|
{ account_number: '1510', debit_amount: 0, credit_amount: 5000, currency: 'SEK' },
|
|
],
|
|
})
|
|
const result = await validateVoucherForInvoiceLink(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
'je-1',
|
|
)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.code).toBe('LINK_VOUCHER_AMOUNT_EXCEEDS_REMAINING')
|
|
})
|
|
|
|
it('rejects when the line currency does not match the invoice', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = setup(makeInvoice({ remaining_amount: 1000, total: 1000, currency: 'EUR' }))
|
|
enqueue({ data: { accounting_method: 'accrual' } }) // resolveAccountingMethod
|
|
enqueue({
|
|
data: {
|
|
id: 'je-1',
|
|
voucher_series: 'A',
|
|
voucher_number: 5,
|
|
entry_date: '2026-05-01',
|
|
description: '',
|
|
status: 'posted',
|
|
source_type: 'manual',
|
|
fiscal_period_id: 'fp-1',
|
|
company_id: 'company-1',
|
|
},
|
|
})
|
|
enqueue({
|
|
data: [
|
|
{ account_number: '1510', debit_amount: 0, credit_amount: 1000, currency: 'SEK' },
|
|
],
|
|
})
|
|
const result = await validateVoucherForInvoiceLink(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
'je-1',
|
|
)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.code).toBe('LINK_VOUCHER_CURRENCY_MISMATCH')
|
|
})
|
|
|
|
it('rejects when the voucher is already linked to this invoice', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = setup()
|
|
enqueue({ data: { accounting_method: 'accrual' } }) // resolveAccountingMethod
|
|
enqueue({
|
|
data: {
|
|
id: 'je-1',
|
|
voucher_series: 'A',
|
|
voucher_number: 5,
|
|
entry_date: '2026-05-01',
|
|
description: '',
|
|
status: 'posted',
|
|
source_type: 'manual',
|
|
fiscal_period_id: 'fp-1',
|
|
company_id: 'company-1',
|
|
},
|
|
})
|
|
enqueue({
|
|
data: [
|
|
{ account_number: '1510', debit_amount: 0, credit_amount: 1000, currency: 'SEK' },
|
|
],
|
|
})
|
|
enqueue({ data: [{ id: 'pmt-1' }] })
|
|
const result = await validateVoucherForInvoiceLink(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
'je-1',
|
|
)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.code).toBe('LINK_VOUCHER_ALREADY_LINKED')
|
|
})
|
|
|
|
it('returns ok=true with full-pay flag when amount equals remaining', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = setup()
|
|
enqueue({ data: { accounting_method: 'accrual' } }) // resolveAccountingMethod
|
|
enqueue({
|
|
data: {
|
|
id: 'je-1',
|
|
voucher_series: 'A',
|
|
voucher_number: 5,
|
|
entry_date: '2026-05-01',
|
|
description: '',
|
|
status: 'posted',
|
|
source_type: 'manual',
|
|
fiscal_period_id: 'fp-1',
|
|
company_id: 'company-1',
|
|
},
|
|
})
|
|
enqueue({
|
|
data: [
|
|
{ account_number: '1930', debit_amount: 1000, credit_amount: 0, currency: 'SEK' },
|
|
{ account_number: '1510', debit_amount: 0, credit_amount: 1000, currency: 'SEK' },
|
|
],
|
|
})
|
|
enqueue({ data: [] })
|
|
const result = await validateVoucherForInvoiceLink(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
'je-1',
|
|
)
|
|
expect(result.ok).toBe(true)
|
|
if (result.ok) {
|
|
expect(result.arCreditAmount).toBe(1000)
|
|
expect(result.paymentAmount).toBe(1000)
|
|
expect(result.isFullyPaid).toBe(true)
|
|
expect(result.remainingAfter).toBe(0)
|
|
}
|
|
})
|
|
|
|
it('returns ok=true with partial-pay flag when amount is less than remaining', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = setup(makeInvoice({ remaining_amount: 1000, total: 1000, currency: 'SEK' }))
|
|
enqueue({ data: { accounting_method: 'accrual' } }) // resolveAccountingMethod
|
|
enqueue({
|
|
data: {
|
|
id: 'je-1',
|
|
voucher_series: 'A',
|
|
voucher_number: 5,
|
|
entry_date: '2026-05-01',
|
|
description: '',
|
|
status: 'posted',
|
|
source_type: 'manual',
|
|
fiscal_period_id: 'fp-1',
|
|
company_id: 'company-1',
|
|
},
|
|
})
|
|
enqueue({
|
|
data: [
|
|
{ account_number: '1510', debit_amount: 0, credit_amount: 400, currency: 'SEK' },
|
|
],
|
|
})
|
|
enqueue({ data: [] })
|
|
const result = await validateVoucherForInvoiceLink(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
'je-1',
|
|
)
|
|
expect(result.ok).toBe(true)
|
|
if (result.ok) {
|
|
expect(result.paymentAmount).toBe(400)
|
|
expect(result.isFullyPaid).toBe(false)
|
|
expect(result.remainingAfter).toBe(600)
|
|
}
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// findMatchingVouchersForInvoice — empty + ranking smoke test
|
|
// ============================================================
|
|
|
|
describe('findMatchingVouchersForInvoice', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('returns empty when the invoice has nothing remaining', async () => {
|
|
const { supabase } = createQueuedMockSupabase()
|
|
const invoice = makeInvoice({ remaining_amount: 0, paid_amount: 1000, total: 1000 })
|
|
const result = await findMatchingVouchersForInvoice(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
)
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('returns empty when the journal lines query errors', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = makeInvoice({
|
|
remaining_amount: 1000,
|
|
total: 1000,
|
|
due_date: '2026-05-01',
|
|
})
|
|
enqueue({ data: { accounting_method: 'accrual' } }) // resolveAccountingMethod
|
|
enqueue({ data: null, error: { message: 'db error' } })
|
|
const result = await findMatchingVouchersForInvoice(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
)
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('cash method: surfaces a verifikat that debits a bank account (19xx), no 1510 needed', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const invoice = makeInvoice({
|
|
remaining_amount: 1000,
|
|
total: 1000,
|
|
currency: 'SEK',
|
|
due_date: '2026-05-01',
|
|
invoice_number: 'F-1',
|
|
})
|
|
enqueue({ data: { accounting_method: 'cash' } }) // resolveAccountingMethod
|
|
// journal_entries query with embedded lines (kontantmetoden: 19xx debit > 0)
|
|
enqueue({
|
|
data: [
|
|
{
|
|
id: 'je-1',
|
|
voucher_series: 'A',
|
|
voucher_number: 7,
|
|
entry_date: '2026-05-01',
|
|
description: 'Betalning faktura F-1',
|
|
status: 'posted',
|
|
source_type: 'manual',
|
|
fiscal_period_id: 'fp-1',
|
|
company_id: 'company-1',
|
|
journal_entry_lines: [
|
|
{
|
|
id: 'l1',
|
|
account_number: '1930',
|
|
debit_amount: 1000,
|
|
credit_amount: 0,
|
|
currency: 'SEK',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
})
|
|
enqueue({ data: [] }) // invoice_payments already-linked lookup
|
|
enqueue({ data: [] }) // fiscal_periods lock lookup
|
|
const result = await findMatchingVouchersForInvoice(
|
|
supabase as never,
|
|
'company-1',
|
|
invoice as never,
|
|
)
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].journal_entry_id).toBe('je-1')
|
|
expect(result[0].ar_credit_amount).toBe(1000)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// linkInvoiceToVoucher — outcome shape & event emission
|
|
// ============================================================
|
|
|
|
describe('linkInvoiceToVoucher', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
eventBus.clear()
|
|
})
|
|
|
|
// linkInvoiceToVoucher now delegates validation + writes to the atomic
|
|
// link_invoice_to_voucher RPC (audit C2) — the wrapper's job is calling it
|
|
// with the right args and mapping the jsonb result/transport errors through.
|
|
// Guard behaviour itself is covered by voucher-matching.pg.test.ts against
|
|
// the real RPC.
|
|
|
|
it('passes a guard rejection from the RPC through unchanged', async () => {
|
|
const rpc = vi.fn().mockResolvedValue({
|
|
data: { ok: false, code: 'LINK_VOUCHER_INVOICE_FULLY_PAID', details: { status: 'paid' } },
|
|
error: null,
|
|
})
|
|
const result = await linkInvoiceToVoucher(
|
|
{ rpc } as never,
|
|
'user-1',
|
|
'company-1',
|
|
{ invoiceId: 'inv-1', journalEntryId: 'je-1' },
|
|
)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) {
|
|
expect(result.code).toBe('LINK_VOUCHER_INVOICE_FULLY_PAID')
|
|
expect(result.details).toEqual({ status: 'paid' })
|
|
}
|
|
expect(rpc).toHaveBeenCalledWith('link_invoice_to_voucher', {
|
|
p_invoice_id: 'inv-1',
|
|
p_journal_entry_id: 'je-1',
|
|
p_user_id: 'user-1',
|
|
p_company_id: 'company-1',
|
|
p_notes: null,
|
|
})
|
|
})
|
|
|
|
it('maps an RPC transport error to LINK_VOUCHER_DB_ERROR', async () => {
|
|
const rpc = vi.fn().mockResolvedValue({ data: null, error: { message: 'connection reset' } })
|
|
const result = await linkInvoiceToVoucher(
|
|
{ rpc } as never,
|
|
'user-1',
|
|
'company-1',
|
|
{ invoiceId: 'inv-1', journalEntryId: 'je-1' },
|
|
)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) {
|
|
expect(result.code).toBe('LINK_VOUCHER_DB_ERROR')
|
|
expect(result.details).toEqual({ reason: 'connection reset' })
|
|
}
|
|
})
|
|
|
|
it('maps an empty RPC response to LINK_VOUCHER_DB_ERROR', async () => {
|
|
const rpc = vi.fn().mockResolvedValue({ data: null, error: null })
|
|
const result = await linkInvoiceToVoucher(
|
|
{ rpc } as never,
|
|
'user-1',
|
|
'company-1',
|
|
{ invoiceId: 'inv-1', journalEntryId: 'je-1' },
|
|
)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.code).toBe('LINK_VOUCHER_DB_ERROR')
|
|
})
|
|
})
|