PHASE 2: Implements balance assertions for reconciliation
Adds balance assertion functionality to enable admins to verify accounting accuracy. This includes: - A new `balance_assertions` table in the database - CRUD operations for balance assertions (create, get, list, check, delete) - API endpoints for managing balance assertions (admin only) - UI elements for creating, viewing, and re-checking assertions Also, reorders the implementation roadmap in the documentation to reflect better the dependencies between phases.
This commit is contained in:
parent
1a9c91d042
commit
0257b7807c
7 changed files with 890 additions and 17 deletions
|
|
@ -251,6 +251,136 @@
|
|||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- Balance Assertions (Super User Only) -->
|
||||
<q-card v-if="isSuperUser">
|
||||
<q-card-section>
|
||||
<div class="row items-center justify-between q-mb-md">
|
||||
<h6 class="q-my-none">Balance Assertions</h6>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="primary"
|
||||
@click="assertionDialog.show = true"
|
||||
icon="add"
|
||||
label="Create Assertion"
|
||||
>
|
||||
<q-tooltip>Create a new balance assertion for reconciliation</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<!-- Failed Assertions -->
|
||||
<div v-if="failedAssertions.length > 0" class="q-mb-md">
|
||||
<q-banner class="bg-negative text-white" rounded>
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="error" color="white" />
|
||||
</template>
|
||||
<div class="text-weight-bold">{% raw %}{{ failedAssertions.length }}{% endraw %} Failed Assertion{% raw %}{{ failedAssertions.length > 1 ? 's' : '' }}{% endraw %}</div>
|
||||
</q-banner>
|
||||
<q-list bordered separator class="q-mt-sm">
|
||||
<q-item v-for="assertion in failedAssertions" :key="assertion.id">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="error" color="negative" size="sm">
|
||||
<q-tooltip>Assertion failed</q-tooltip>
|
||||
</q-icon>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Account: {% raw %}{{ getAccountName(assertion.account_id) }}{% endraw %}</q-item-label>
|
||||
<q-item-label caption>Expected: {% raw %}{{ formatSats(assertion.expected_balance_sats) }}{% endraw %} sats</q-item-label>
|
||||
<q-item-label caption>Actual: {% raw %}{{ formatSats(assertion.checked_balance_sats) }}{% endraw %} sats</q-item-label>
|
||||
<q-item-label caption class="text-negative">Difference: {% raw %}{{ formatSats(assertion.difference_sats) }}{% endraw %} sats</q-item-label>
|
||||
<q-item-label caption v-if="assertion.fiat_currency">
|
||||
Expected: {% raw %}{{ assertion.expected_balance_fiat }} {{ assertion.fiat_currency }}{% endraw %} |
|
||||
Actual: {% raw %}{{ assertion.checked_balance_fiat }} {{ assertion.fiat_currency }}{% endraw %} |
|
||||
Difference: {% raw %}{{ assertion.difference_fiat }} {{ assertion.fiat_currency }}{% endraw %}
|
||||
</q-item-label>
|
||||
<q-item-label caption>Checked: {% raw %}{{ formatDate(assertion.checked_at) }}{% endraw %}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<div class="q-gutter-xs">
|
||||
<q-btn
|
||||
size="sm"
|
||||
flat
|
||||
round
|
||||
icon="refresh"
|
||||
@click="recheckAssertion(assertion.id)"
|
||||
:loading="assertion.rechecking"
|
||||
>
|
||||
<q-tooltip>Re-check assertion</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
size="sm"
|
||||
flat
|
||||
round
|
||||
icon="delete"
|
||||
@click="deleteAssertion(assertion.id)"
|
||||
:loading="assertion.deleting"
|
||||
>
|
||||
<q-tooltip>Delete assertion</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
|
||||
<!-- Passed Assertions -->
|
||||
<div v-if="passedAssertions.length > 0">
|
||||
<q-expansion-item
|
||||
label="Passed Assertions"
|
||||
:caption="`${passedAssertions.length} assertion${passedAssertions.length > 1 ? 's' : ''} passed`"
|
||||
icon="check_circle"
|
||||
header-class="text-positive"
|
||||
>
|
||||
<q-list bordered separator>
|
||||
<q-item v-for="assertion in passedAssertions" :key="assertion.id">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="check_circle" color="positive" size="sm">
|
||||
<q-tooltip>Assertion passed</q-tooltip>
|
||||
</q-icon>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{% raw %}{{ getAccountName(assertion.account_id) }}{% endraw %}</q-item-label>
|
||||
<q-item-label caption>Balance: {% raw %}{{ formatSats(assertion.checked_balance_sats) }}{% endraw %} sats</q-item-label>
|
||||
<q-item-label caption v-if="assertion.fiat_currency">
|
||||
Fiat: {% raw %}{{ assertion.checked_balance_fiat }} {{ assertion.fiat_currency }}{% endraw %}
|
||||
</q-item-label>
|
||||
<q-item-label caption>Checked: {% raw %}{{ formatDate(assertion.checked_at) }}{% endraw %}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<div class="q-gutter-xs">
|
||||
<q-btn
|
||||
size="sm"
|
||||
flat
|
||||
round
|
||||
icon="refresh"
|
||||
@click="recheckAssertion(assertion.id)"
|
||||
:loading="assertion.rechecking"
|
||||
>
|
||||
<q-tooltip>Re-check assertion</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
size="sm"
|
||||
flat
|
||||
round
|
||||
icon="delete"
|
||||
@click="deleteAssertion(assertion.id)"
|
||||
:loading="assertion.deleting"
|
||||
>
|
||||
<q-tooltip>Delete assertion</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-expansion-item>
|
||||
</div>
|
||||
|
||||
<!-- No assertions message -->
|
||||
<div v-if="balanceAssertions.length === 0" class="text-center text-grey q-pa-md">
|
||||
No balance assertions yet. Create one to verify your accounting accuracy.
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
|
|
@ -761,4 +891,110 @@
|
|||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Balance Assertion Dialog -->
|
||||
<q-dialog v-model="assertionDialog.show" position="top">
|
||||
<q-card v-if="assertionDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="submitAssertion" class="q-gutter-md">
|
||||
<div class="text-h6 q-mb-md">Create Balance Assertion</div>
|
||||
|
||||
<div class="text-caption text-grey q-mb-md">
|
||||
Balance assertions help you verify accounting accuracy by checking if an account's actual balance matches your expected balance. If the assertion fails, you'll be alerted to investigate the discrepancy.
|
||||
</div>
|
||||
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="assertionDialog.account_id"
|
||||
:options="allAccounts"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
emit-value
|
||||
map-options
|
||||
label="Account *"
|
||||
:rules="[val => !!val || 'Account is required']"
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section>
|
||||
<q-item-label>{% raw %}{{ scope.opt.name }}{% endraw %}</q-item-label>
|
||||
<q-item-label caption>{% raw %}{{ scope.opt.account_type }}{% endraw %}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="assertionDialog.expected_balance_sats"
|
||||
type="number"
|
||||
label="Expected Balance (sats) *"
|
||||
:rules="[val => val !== null && val !== undefined || 'Expected balance is required']"
|
||||
>
|
||||
<template v-slot:hint>
|
||||
The balance you expect this account to have in satoshis
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="assertionDialog.tolerance_sats"
|
||||
type="number"
|
||||
label="Tolerance (sats)"
|
||||
min="0"
|
||||
>
|
||||
<template v-slot:hint>
|
||||
Allow the actual balance to differ by ± this amount (default: 0)
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<div class="text-subtitle2">Optional: Fiat Balance Check</div>
|
||||
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="assertionDialog.fiat_currency"
|
||||
:options="currencyOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
emit-value
|
||||
map-options
|
||||
label="Fiat Currency (optional)"
|
||||
clearable
|
||||
></q-select>
|
||||
|
||||
<q-input
|
||||
v-if="assertionDialog.fiat_currency"
|
||||
filled
|
||||
dense
|
||||
v-model.number="assertionDialog.expected_balance_fiat"
|
||||
type="number"
|
||||
step="0.01"
|
||||
:label="`Expected Fiat Balance (${assertionDialog.fiat_currency})`"
|
||||
></q-input>
|
||||
|
||||
<q-input
|
||||
v-if="assertionDialog.fiat_currency"
|
||||
filled
|
||||
dense
|
||||
v-model.number="assertionDialog.tolerance_fiat"
|
||||
type="number"
|
||||
step="0.01"
|
||||
label="Fiat Tolerance"
|
||||
min="0"
|
||||
></q-input>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated color="primary" type="submit" :loading="assertionDialog.loading">
|
||||
Create & Check
|
||||
</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue