LP1904036 Billing continued
authorBill Erickson <berickxx@gmail.com>
Mon, 8 Mar 2021 16:57:19 +0000 (11:57 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:25 +0000 (20:13 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm

index 5352312..13697c6 100644 (file)
@@ -1,3 +1,15 @@
+<eg-prompt-dialog #annotateDialog
+  i18n-dialogBody dialogBody="Please Annotate This Payment">
+</eg-prompt-dialog>
+
+<eg-alert-dialog #maxPayDialog
+  i18n-dialogBody dialogBody="Payments over 100,000 are denied by policy.">
+</eg-alert-dialog>
+<eg-confirm-dialog #warnPayDialog
+  i18n-dialogBody i18n-dialogTitle dialogTitle="Confirm Payment Amount"
+  dialogBody="Are you sure you want to apply a payment of {{payAmount | currency}}?">
+</eg-confirm-dialog>
+
 
 <!-- SUMMARY  -->
 
         id="pay-amount" [min]="0"/>
     </div>
     <div class="ml-2 form-check form-check-inline">
-      <input class="form-check-input" type="checkbox" id="annotate" [(ngModel)]="annotatePayment"/>
+      <input class="form-check-input" type="checkbox" 
+        id="annotate" [(ngModel)]="annotatePayment"/>
       <label class="form-check-label" for="annotate" i18n>Annotate</label>
     </div>
     <div class="ml-2">
index 9feb702..8279c01 100644 (file)
@@ -4,6 +4,7 @@ import {from, empty} from 'rxjs';
 import {concatMap, tap, takeLast} from 'rxjs/operators';
 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 import {IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
 import {NetService} from '@eg/core/net.service';
 import {PcrudService, PcrudContext} from '@eg/core/pcrud.service';
 import {AuthService} from '@eg/core/auth.service';
@@ -14,6 +15,9 @@ import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/
 import {GridComponent} from '@eg/share/grid/grid.component';
 import {Pager} from '@eg/share/util/pager';
 import {CircService, CircDisplayInfo} from '@eg/staff/share/circ/circ.service';
+import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
 
 interface BillGridEntry extends CircDisplayInfo {
     xact: IdlObject // mbt
@@ -55,20 +59,30 @@ export class BillsComponent implements OnInit, AfterViewInit {
     checkNumber: string;
     payAmount: number;
     annotatePayment = false;
+    annotation: string;
     entries: BillGridEntry[];
+    convertChangeToCredit = false;
+    receiptOnPayment = false;
+
+    maxPayAmount = 100000;
+    warnPayAmount = 1000;
 
     gridDataSource: GridDataSource = new GridDataSource();
     cellTextGenerator: GridCellTextGenerator;
 
     @ViewChild('billGrid') private billGrid: GridComponent;
+    @ViewChild('annotateDialog') private annotateDialog: PromptDialogComponent;
+    @ViewChild('maxPayDialog') private maxPayDialog: AlertDialogComponent;
+    @ViewChild('warnPayDialog') private warnPayDialog: ConfirmDialogComponent;
 
     constructor(
         private router: Router,
         private route: ActivatedRoute,
+        private org: OrgService,
         private net: NetService,
         private pcrud: PcrudService,
         private auth: AuthService,
-        private store: ServerStoreService,
+        private serverStore: ServerStoreService,
         private circ: CircService,
         public patronService: PatronService,
         public context: PatronContextService
@@ -94,12 +108,28 @@ export class BillsComponent implements OnInit, AfterViewInit {
             return from(page);
         };
 
-        this.load();
+        this.applySettings().then(_ => this.load());
+    }
+
+    applySettings(): Promise<any> {
+        return this.serverStore.getItemBatch([
+            'ui.circ.billing.amount_warn',
+            'ui.circ.billing.amount_limit'
+        ]).then(sets => {
+            this.maxPayAmount = sets['ui.circ.billing.amount_limit'] || 100000;
+            this.warnPayAmount = sets['ui.circ.billing.amount_warn'] || 1000;
+        });
     }
 
     ngAfterViewInit() {
-        const node = document.getElementById('pay-amount');
-        if (node) { node.focus(); }
+        this.focusPayAmount();
+    }
+
+    focusPayAmount() {
+        setTimeout(() => {
+            const node = document.getElementById('pay-amount') as HTMLInputElement;
+            if (node) { node.focus(); node.select(); }
+        });
     }
 
     load() {
@@ -135,12 +165,19 @@ export class BillsComponent implements OnInit, AfterViewInit {
         };
 
         if (xact.summary().xact_type() !== 'circulation') {
+
+            entry.xact.grocery().billing_location(
+                this.org.get(entry.xact.grocery().billing_location()));
+
             entry.title = xact.summary().last_billing_type();
             entry.billingLocation =
                 xact.grocery().billing_location().shortname();
             return entry;
         }
 
+        entry.xact.circulation().circ_lib(
+            this.org.get(entry.xact.circulation().circ_lib()));
+
         const circDisplay: CircDisplayInfo =
             this.circ.getDisplayInfo(xact.circulation());
 
@@ -154,43 +191,125 @@ export class BillsComponent implements OnInit, AfterViewInit {
         return this.context.patron;
     }
 
+    selectedPaymentInfo(): {owed: number, billed: number, paid: number} {
+        const info = {owed : 0, billed : 0, paid : 0};
+
+        this.billGrid.context.rowSelector.selected().forEach(id => {
+            const row = this.billGrid.context.getRowByIndex(id);
+            const sum = row.xact.summary();
+
+            info.owed   += Number(sum.balance_owed()) * 100;
+            info.billed += Number(sum.total_owed()) * 100;
+            info.paid   += Number(sum.total_paid()) * 100;
+        });
+
+        info.owed /= 100;
+        info.billed /= 100;
+        info.paid /= 100;
+
+        return info;
+    }
+
+    pendingPaymentInfo(): {payment: number, change: number} {
+
+        const amt = this.payAmount || 0;
+
+        if (amt >= this.paidSelected()) {
+            const owedSelected = this.owedSelected();
+            return {
+                payment : this.owedSelected(),
+                change : amt - owedSelected
+            }
+        }
+
+        return {payment : amt, change : 0};
+    }
+
     disablePayment(): boolean {
         if (!this.billGrid) { return true; } // still loading
 
-        // TODO: pay amount can be zero when refunding
         return (
-            this.payAmount <= 0 ||
+            this.payAmount === 0 ||
+            (this.payAmount < 0 && this.paymentType !== 'refund') ||
             this.billGrid.context.rowSelector.selected().length === 0
         );
     }
 
-    // TODO
     refundsAvailable(): number {
-        return 0;
+        let amount = 0;
+        this.gridDataSource.data.forEach(row => {
+            const balance = row.xact.summary().balance_owed();
+            if (balance < 0) { amount += balance * 100; }
+
+        });
+
+        return -(amount / 100);
     }
 
-    // TODO
     paidSelected(): number {
-        return 0;
+        return this.selectedPaymentInfo().paid;
     }
 
-    // TODO
     owedSelected(): number {
-        return 0;
+        return this.selectedPaymentInfo().owed;
     }
 
-    // TODO
     billedSelected(): number {
-        return 0;
+        return this.selectedPaymentInfo().billed;
     }
 
     pendingPayment(): number {
-        return 0;
+        return this.pendingPaymentInfo().payment;
     }
 
     pendingChange(): number {
-        return 0;
+        return this.pendingPaymentInfo().change;
+    }
+
+    applyPayment() {
+
+        if (this.amountExceedsMax()) { return; }
+
+        this.annotation = '';
+
+        this.verifyPayAmount()
+        .then(_ => this.annotate())
+        .then(_ => this.addCcArgs())
+        .catch(err => console.debug('Payment was canceled:', err));
+    }
+
+    amountExceedsMax(): boolean {
+        if (this.payAmount < this.maxPayAmount) { return false; }
+        this.maxPayDialog.open().toPromise().then(_ => this.focusPayAmount());
+        return true;
     }
 
+    addCcArgs(): Promise<any> {
+        return null;
+    }
+
+    verifyPayAmount(): Promise<any> {
+        if (this.payAmount < this.warnPayAmount) {
+            return Promise.resolve();
+        }
+
+        return this.warnPayDialog.open().toPromise().then(confirmed => {
+            if (!confirmed) {
+                return Promise.reject('Pay amount not confirmed');
+            }
+        });
+    }
+
+    annotate(): Promise<any> {
+        if (!this.annotatePayment) { return Promise.resolve(); }
+
+        return this.annotateDialog.open().toPromise()
+        .then(value => {
+            if (!value) {
+                return Promise.reject('No annotation supplied');
+            }
+            this.annotation = value;
+        });
+    }
 }
 
index f6b97d0..6af395d 100644 (file)
@@ -5396,7 +5396,6 @@ sub user_billing_xacts {
                         target_copy 
                         workstation 
                         checkin_workstation 
-                        circ_lib
                     /],
                                    acp =>  [qw/
                                            call_number