Validate user input amounts and fail gracefully
This commit is contained in:
parent
ecce02616c
commit
83ca78436c
@ -47,14 +47,26 @@
|
||||
<button class="btn-secondary text-red-600 ring-red-500" @click="deleteAssertion" v-if="assertion.id !== null">Delete</button>
|
||||
<button class="btn-primary" @click="saveAssertion">Save</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md bg-red-50 mt-4 p-4 col-span-2" v-if="error !== null">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-400" />
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm text-red-700">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { DT_FORMAT, db, deserialiseAmount } from '../db.ts';
|
||||
import { DeserialiseAmountError, DT_FORMAT, db, deserialiseAmount } from '../db.ts';
|
||||
import ComboBoxAccounts from './ComboBoxAccounts.vue';
|
||||
|
||||
export interface EditingAssertion {
|
||||
@ -68,9 +80,23 @@
|
||||
|
||||
const { assertion } = defineProps<{ assertion: EditingAssertion }>();
|
||||
|
||||
const error = ref(null as string | null);
|
||||
|
||||
async function saveAssertion() {
|
||||
// Save changes to the assertion
|
||||
const amount_abs = deserialiseAmount('' + assertion.amount_abs);
|
||||
error.value = null;
|
||||
|
||||
let amount_abs;
|
||||
try {
|
||||
amount_abs = deserialiseAmount('' + assertion.amount_abs);
|
||||
} catch (err) {
|
||||
if (err instanceof DeserialiseAmountError) {
|
||||
error.value = err.message;
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const quantity = assertion.sign === 'dr' ? amount_abs.quantity : -amount_abs.quantity;
|
||||
|
||||
const session = await db.load();
|
||||
@ -90,6 +116,8 @@
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Send event
|
||||
|
||||
await getCurrentWindow().close();
|
||||
}
|
||||
|
||||
@ -107,6 +135,8 @@
|
||||
[assertion.id]
|
||||
);
|
||||
|
||||
// TODO: Send event
|
||||
|
||||
await getCurrentWindow().close();
|
||||
}
|
||||
</script>
|
||||
|
@ -109,7 +109,7 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { DT_FORMAT, Posting, Transaction, db, deserialiseAmount } from '../db.ts';
|
||||
import { DT_FORMAT, DeserialiseAmountError, Posting, Transaction, db, deserialiseAmount } from '../db.ts';
|
||||
import ComboBoxAccounts from './ComboBoxAccounts.vue';
|
||||
|
||||
interface EditingPosting {
|
||||
@ -155,7 +155,17 @@
|
||||
);
|
||||
|
||||
for (const posting of transaction.postings) {
|
||||
const amount_abs = deserialiseAmount(posting.amount_abs);
|
||||
let amount_abs;
|
||||
try {
|
||||
amount_abs = deserialiseAmount(posting.amount_abs);
|
||||
} catch (err) {
|
||||
if (err instanceof DeserialiseAmountError) {
|
||||
error.value = err.message;
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
newTransaction.postings.push({
|
||||
id: posting.id,
|
||||
|
@ -176,7 +176,8 @@ export function deserialiseAmount(amount: string): { quantity: number, commodity
|
||||
// Default commodity
|
||||
const quantity = Math.round(parseFloat(amount) * factor)
|
||||
|
||||
if (!Number.isSafeInteger(quantity)) { throw new Error('Quantity not representable by safe integer'); }
|
||||
if (Number.isNaN(quantity)) { throw new DeserialiseAmountError('Invalid quantity: ' + amount); }
|
||||
if (!Number.isSafeInteger(quantity)) { throw new DeserialiseAmountError('Quantity not representable by safe integer: ' + amount); }
|
||||
|
||||
return {
|
||||
'quantity': quantity,
|
||||
@ -189,7 +190,8 @@ export function deserialiseAmount(amount: string): { quantity: number, commodity
|
||||
const quantityStr = amount.substring(0, amount.indexOf(' '));
|
||||
const quantity = Math.round(parseFloat(quantityStr) * factor)
|
||||
|
||||
if (!Number.isSafeInteger(quantity)) { throw new Error('Quantity not representable by safe integer'); }
|
||||
if (Number.isNaN(quantity)) { throw new DeserialiseAmountError('Invalid quantity: ' + amount); }
|
||||
if (!Number.isSafeInteger(quantity)) { throw new DeserialiseAmountError('Quantity not representable by safe integer: ' + amount); }
|
||||
|
||||
const commodity = amount.substring(amount.indexOf(' ') + 1);
|
||||
|
||||
@ -199,6 +201,8 @@ export function deserialiseAmount(amount: string): { quantity: number, commodity
|
||||
};
|
||||
}
|
||||
|
||||
export class DeserialiseAmountError extends Error {}
|
||||
|
||||
// Type definitions
|
||||
|
||||
export class Transaction {
|
||||
|
@ -60,14 +60,27 @@
|
||||
<button class="btn-secondary text-red-600 ring-red-500" @click="deleteAdjustment" v-if="adjustment.id !== null">Delete</button>
|
||||
<button class="btn-primary" @click="saveAdjustment">Save</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md bg-red-50 mt-4 p-4 col-span-2" v-if="error !== null">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-400" />
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm text-red-700">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import ComboBoxAccounts from '../../components/ComboBoxAccounts.vue';
|
||||
import { DT_FORMAT, db, deserialiseAmount } from '../../db.ts';
|
||||
import { DT_FORMAT, DeserialiseAmountError, db, deserialiseAmount } from '../../db.ts';
|
||||
|
||||
export interface EditingCGTAdjustment {
|
||||
id: number | null,
|
||||
@ -82,10 +95,36 @@
|
||||
|
||||
const { adjustment } = defineProps<{ adjustment: EditingCGTAdjustment }>();
|
||||
|
||||
const error = ref(null as string | null);
|
||||
|
||||
async function saveAdjustment() {
|
||||
// Save changes to the CGT adjustment
|
||||
const asset = deserialiseAmount('' + adjustment.asset);
|
||||
const cost_adjustment_abs = deserialiseAmount('' + adjustment.cost_adjustment_abs);
|
||||
error.value = null;
|
||||
|
||||
let asset;
|
||||
try {
|
||||
asset = deserialiseAmount(adjustment.asset);
|
||||
} catch (err) {
|
||||
if (err instanceof DeserialiseAmountError) {
|
||||
error.value = err.message;
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
let cost_adjustment_abs;
|
||||
try {
|
||||
cost_adjustment_abs = deserialiseAmount(adjustment.cost_adjustment_abs);
|
||||
} catch (err) {
|
||||
if (err instanceof DeserialiseAmountError) {
|
||||
error.value = err.message;
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const cost_adjustment = adjustment.sign === 'dr' ? cost_adjustment_abs.quantity : -cost_adjustment_abs.quantity;
|
||||
|
||||
const session = await db.load();
|
||||
|
@ -70,18 +70,31 @@
|
||||
<div class="flex justify-end mt-4 space-x-2">
|
||||
<button class="btn-primary" @click="saveAdjustment">Save</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md bg-red-50 mt-4 p-4 col-span-2" v-if="error !== null">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-400" />
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm text-red-700">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { InformationCircleIcon } from '@heroicons/vue/20/solid';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { CGTAsset } from './cgt.ts';
|
||||
import ComboBoxAccounts from '../../components/ComboBoxAccounts.vue';
|
||||
import { DT_FORMAT, JoinedTransactionPosting, db, deserialiseAmount } from '../../db.ts';
|
||||
import { DT_FORMAT, DeserialiseAmountError, JoinedTransactionPosting, db, deserialiseAmount } from '../../db.ts';
|
||||
import { ppWithCommodity } from '../../display.ts';
|
||||
import { CriticalError } from '../../error.ts';
|
||||
|
||||
const account = ref('');
|
||||
const commodity = ref('');
|
||||
@ -90,10 +103,25 @@
|
||||
const cost_adjustment_abs = ref(null! as number);
|
||||
const sign = ref('dr');
|
||||
|
||||
const error = ref(null as string | null);
|
||||
|
||||
async function saveAdjustment() {
|
||||
// TODO: Preview mode?
|
||||
|
||||
const totalAdjustmentAbs = deserialiseAmount('' + cost_adjustment_abs.value);
|
||||
error.value = null;
|
||||
|
||||
let totalAdjustmentAbs;
|
||||
try {
|
||||
totalAdjustmentAbs = deserialiseAmount('' + cost_adjustment_abs.value);
|
||||
} catch (err) {
|
||||
if (err instanceof DeserialiseAmountError) {
|
||||
error.value = err.message;
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const totalAdjustment = sign.value === 'dr' ? totalAdjustmentAbs.quantity : -totalAdjustmentAbs.quantity;
|
||||
|
||||
// Get all postings to the CGT asset account
|
||||
@ -149,7 +177,8 @@
|
||||
}
|
||||
|
||||
if (assets.length === 0) {
|
||||
throw new Error('No matching CGT assets');
|
||||
error.value = 'No matching CGT assets';
|
||||
return;
|
||||
}
|
||||
|
||||
// Distribute total adjustment across matching assets
|
||||
@ -192,7 +221,7 @@
|
||||
// Sanity check
|
||||
const totalRoundedAdjustment = cgtAdjustments.reduce((acc, adj) => acc + adj, 0);
|
||||
if (totalRoundedAdjustment !== totalAdjustment) {
|
||||
throw new Error('Rounding unexpectedly changed total CGT adjustment amount');
|
||||
throw new CriticalError('Rounding unexpectedly changed total CGT adjustment amount');
|
||||
}
|
||||
|
||||
// Add adjustments to database atomically
|
||||
|
Loading…
x
Reference in New Issue
Block a user