@zeniai/client-epic-state 5.1.42-betaDI4 → 5.1.43-betaDI5
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/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
- package/lib/entity/snackbar/snackbarTypes.js +1 -0
- package/lib/entity/tenant/epic/saveExternalConnectionEpic.js +15 -11
- package/lib/esm/entity/snackbar/snackbarTypes.js +1 -0
- package/lib/esm/entity/tenant/epic/saveExternalConnectionEpic.js +15 -11
- package/lib/esm/index.js +4 -4
- package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +7 -0
- package/lib/esm/view/transactionDetail/epics/saveTransactionHelper.js +22 -0
- package/lib/esm/view/transactionDetail/epics/updateTransactionDetailEpic.js +75 -3
- package/lib/esm/view/transactionDetail/journalEntryLinesViewModel.js +58 -0
- package/lib/esm/view/transactionDetail/transactionDetailLocalDataHelper.js +10 -8
- package/lib/esm/view/transactionDetail/transactionDetailReducer.js +66 -1
- package/lib/index.d.ts +4 -4
- package/lib/index.js +32 -28
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +7 -0
- package/lib/view/transactionDetail/epics/saveTransactionHelper.js +22 -0
- package/lib/view/transactionDetail/epics/updateTransactionDetailEpic.d.ts +5 -1
- package/lib/view/transactionDetail/epics/updateTransactionDetailEpic.js +77 -4
- package/lib/view/transactionDetail/journalEntryLinesViewModel.d.ts +27 -0
- package/lib/view/transactionDetail/journalEntryLinesViewModel.js +61 -1
- package/lib/view/transactionDetail/transactionDetailLocalDataHelper.js +10 -8
- package/lib/view/transactionDetail/transactionDetailReducer.d.ts +11 -2
- package/lib/view/transactionDetail/transactionDetailReducer.js +67 -2
- package/lib/view/transactionDetail/transactionDetailState.d.ts +8 -1
- package/lib/view/transactionDetail/transactionDetailTypes.d.ts +2 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const toSnackbarMessageSection: (v: string) => "common" | "transactionDetails_updatingPastTransactions" | "transactionDetails_transactionUpdated" | "cockpit_month_end_email_sent" | "cockpit_month_end_email_save" | "cockpit_month_end_email_attachment_upload" | "je_bill_link" | "je_posted" | "audit_score_updated" | "people_invite_people" | "people_invite_multiple_people" | "people_delete_person" | "people_update_person" | "reimbursement_updated" | "reimbursement_deleted" | "reimbursement_cancelled_deleted" | "reimbursement_cancelled" | "reimbursement_approved" | "reimbursement_rejected" | "reimbursement_sent_for_approval" | "account_added" | "business_verification_save" | "business_verification_submit" | "business_verification_from_bills_submit" | "plaid_connection" | "unlink_deposit_account" | "reimbursement_setup" | "reimbursement_approval_create" | "reimbursement_duplicate_approval_create" | "reimbursement_approval_update" | "reimbursement_duplicate_approval_update" | "reimbursement_approval_delete" | "reimbursement_approval_reorder" | "reimbursement_create_mileage" | "reimbursement_update_mileage" | "reimbursement_accept_term" | "reimbursement_accept_employee_term" | "reimbursement_bulk_submit" | "reimbursement_bulk_processed" | "bill_pay_setup" | "zeni_accounts_setup" | "bill_pay_approval_create" | "bill_pay_duplicate_approval_create" | "bill_pay_approval_update" | "bill_pay_duplicate_approval_update" | "bill_pay_approval_delete" | "bill_pay_approval_reorder" | "bill_pay_updated" | "bill_pay_deleted" | "bill_pay_cancelled_deleted" | "bill_pay_cancelled" | "bill_pay_approved" | "real_time_approver_added" | "bill_pay_rejected" | "bill_pay_sent_for_approval" | "bill_pay_accept_term" | "bill_pay_bulk_submit" | "bill_pay_bulk_processed" | "bill_pay_refund" | "bill_pay_retry" | "bill_pay_marked_as_paid" | "zeni_account_accept_term" | "update_vendor" | "data_refresh_update" | "delete_bank_account" | "create_bank_account" | "create_bank_account_ach" | "create_bank_account_wire" | "create_bank_account_international" | "transfer_money" | "update_zeni_account_nickname" | "create_checking_account" | "deposit_check" | "create_vendor" | "onboarding_customer_view" | "onboarding_customer_view_complete" | "onboarding_customer_identity_verification_save" | "onboarding_customer_identity_verification_submit" | "onboarding_customer_business_verification_save" | "onboarding_customer_business_verification_submit" | "retry_bank_account_connection" | "dashboard_invite_sent" | "onboarding_info_saved" | "approve_original_merchant" | "approve_global_merchant" | "reject_global_merchant" | "create_global_merchant" | "fetch_global_merchant_no_recommendation" | "save_vendor_renamed" | "save_vendor_sent_for_review" | "save_vendor_marked_as_employee" | "save_vendor_marked_as_local_contractor" | "charge_card_setup" | "charge_card_accept_term" | "charge_card_receipt_upload" | "charge_card_resend_invite" | "charge_card_revoke_invite" | "card_policy_created" | "card_policy_updated" | "card_policy_deleted" | "charge_card_update_limit" | "charge_cards_update_limit" | "close_charge_card" | "lock_charge_card" | "lock_charge_card_card_user" | "lock_charge_cards" | "unlock_charge_card" | "unlock_charge_card_card_user" | "unlock_charge_cards" | "close_charge_cards" | "revoke_invite_charge_cards" | "charge_card_express_interest" | "create_schedule" | "save_schedule" | "delete_schedule" | "ignore_schedule" | "save_task_detail" | "fetch_task_detail" | "delete_task" | "archive_task" | "snooze_task" | "unsnooze_task" | "create_tag" | "delete_tag" | "update_charge_card" | "update_charge_card_name" | "update_charge_card_name_card_user" | "issue_charge_card" | "issue_charge_cards" | "notification_settings_saved" | "referral_invite_sent" | "notification_mark_as_read" | "mark_as_complete_schedule" | "cancel_journal_entry" | "settings_accounting_accounts_updated" | "create_card_setup" | "confirm_card_setup" | "add_card_payment_source" | "fetch_payment_sources" | "task_assigned_toast_notification" | "task_due_tomorrow_toast_notification" | "task_deleted_toast_notification" | "task_archived_toast_notification" | "task_overdue_toast_notification" | "task_overdue_toast_notification_creator" | "task_notification_count" | "task_activities_toast_notification" | "task_created_toast_notification" | "task_group_creation_success" | "task_group_deletion_success" | "task_group_update_success" | "primary_funding_account_updated" | "task_time_spent_validation" | "missing_receipts_attachment" | "flux_analysis_unreviewed" | "flux_analysis_reviewed" | "receipt_match" | "receipts_upload" | "receipts_bulk_match" | "billing_address_view" | "express_pay_submit" | "exclude_transaction" | "reconcile" | "save_reconcile_for_later" | "recon_transaction_categorize" | "recon_transaction_match" | "update_debit_card_pin_attempt" | "set_debit_card_pin" | "ai_cfo_create_session_and_submit" | "ai_cfo_chat_session_deleted" | "charge_card_auto_pay_enable" | "charge_card_auto_pay_disable" | "treasury_setup" | "treasury_accept_term" | "treasury_transfer_money" | "treasury_transfer_money_failed" | "auto_transfer_rule_create" | "auto_transfer_rule_update" | "auto_transfer_rule_delete" | "auto_transfer_rule_pause" | "auto_transfer_rule_resume" | "treasury_update_portfolio_allocation" | "send_email_magic_link_to_user" | "complete_profile_done" | "create_transfer_entry" | "reports_resync" | "invalid_phone_number" | "transactions_categorized_updated_failed" | "transaction_categorized_updated_failed" | "transactionsCategorized_transactionsUpdated" | "transactionsCategorized_transactionUpdated" | "transactionCategorized_transactionsUpdated" | "transactionCategorized_transactionUpdated" | "transactionsCategorized_transactionsFailed" | "transactionsCategorized_transactionFailed" | "transactionCategorized_transactionsFailed" | "transactionCategorized_transactionFailed" | "transactionsUpdated_transactionsFailed" | "transactionsUpdated_transactionFailed" | "transactionUpdated_transactionsFailed" | "transactionUpdated_transactionFailed" | "transactionsCategorized_transactionsUpdated_transactionsFailed" | "transactionsCategorized_transactionsUpdated_transactionFailed" | "transactionsCategorized_transactionUpdated_transactionsFailed" | "transactionCategorized_transactionsUpdated_transactionsFailed" | "transactionsCategorized_transactionUpdated_transactionFailed" | "transactionCategorized_transactionsUpdated_transactionFailed" | "transactionCategorized_transactionUpdated_transactionsFailed" | "transactionCategorized_transactionUpdated_transactionFailed" | "accounting_classes_enabled_update" | "capitalization_threshold_update" | "account_excluded_from_reconciliation" | "account_included_in_reconciliation" | "statement_parsing_in_progress" | "statement_parsing_complete" | "ai_accountant_trigger_job" | "oauth_consent_approve" | "oauth_consent_invalid_request" | "credit_agent_update_profile" | "credit_agent_save_macro" | "credit_agent_schedule_cron" | "create_sub_task" | "qbo_connection_already_linked";
|
|
1
|
+
export declare const toSnackbarMessageSection: (v: string) => "common" | "transactionDetails_updatingPastTransactions" | "transactionDetails_transactionUpdated" | "cockpit_month_end_email_sent" | "cockpit_month_end_email_save" | "cockpit_month_end_email_attachment_upload" | "je_bill_link" | "je_posted" | "audit_score_updated" | "people_invite_people" | "people_invite_multiple_people" | "people_delete_person" | "people_update_person" | "reimbursement_updated" | "reimbursement_deleted" | "reimbursement_cancelled_deleted" | "reimbursement_cancelled" | "reimbursement_approved" | "reimbursement_rejected" | "reimbursement_sent_for_approval" | "account_added" | "business_verification_save" | "business_verification_submit" | "business_verification_from_bills_submit" | "plaid_connection" | "unlink_deposit_account" | "reimbursement_setup" | "reimbursement_approval_create" | "reimbursement_duplicate_approval_create" | "reimbursement_approval_update" | "reimbursement_duplicate_approval_update" | "reimbursement_approval_delete" | "reimbursement_approval_reorder" | "reimbursement_create_mileage" | "reimbursement_update_mileage" | "reimbursement_accept_term" | "reimbursement_accept_employee_term" | "reimbursement_bulk_submit" | "reimbursement_bulk_processed" | "bill_pay_setup" | "zeni_accounts_setup" | "bill_pay_approval_create" | "bill_pay_duplicate_approval_create" | "bill_pay_approval_update" | "bill_pay_duplicate_approval_update" | "bill_pay_approval_delete" | "bill_pay_approval_reorder" | "bill_pay_updated" | "bill_pay_deleted" | "bill_pay_cancelled_deleted" | "bill_pay_cancelled" | "bill_pay_approved" | "real_time_approver_added" | "bill_pay_rejected" | "bill_pay_sent_for_approval" | "bill_pay_accept_term" | "bill_pay_bulk_submit" | "bill_pay_bulk_processed" | "bill_pay_refund" | "bill_pay_retry" | "bill_pay_marked_as_paid" | "zeni_account_accept_term" | "update_vendor" | "data_refresh_update" | "delete_bank_account" | "create_bank_account" | "create_bank_account_ach" | "create_bank_account_wire" | "create_bank_account_international" | "transfer_money" | "update_zeni_account_nickname" | "create_checking_account" | "deposit_check" | "create_vendor" | "onboarding_customer_view" | "onboarding_customer_view_complete" | "onboarding_customer_identity_verification_save" | "onboarding_customer_identity_verification_submit" | "onboarding_customer_business_verification_save" | "onboarding_customer_business_verification_submit" | "retry_bank_account_connection" | "dashboard_invite_sent" | "onboarding_info_saved" | "approve_original_merchant" | "approve_global_merchant" | "reject_global_merchant" | "create_global_merchant" | "fetch_global_merchant_no_recommendation" | "save_vendor_renamed" | "save_vendor_sent_for_review" | "save_vendor_marked_as_employee" | "save_vendor_marked_as_local_contractor" | "charge_card_setup" | "charge_card_accept_term" | "charge_card_receipt_upload" | "charge_card_resend_invite" | "charge_card_revoke_invite" | "card_policy_created" | "card_policy_updated" | "card_policy_deleted" | "charge_card_update_limit" | "charge_cards_update_limit" | "close_charge_card" | "lock_charge_card" | "lock_charge_card_card_user" | "lock_charge_cards" | "unlock_charge_card" | "unlock_charge_card_card_user" | "unlock_charge_cards" | "close_charge_cards" | "revoke_invite_charge_cards" | "charge_card_express_interest" | "create_schedule" | "save_schedule" | "delete_schedule" | "ignore_schedule" | "save_task_detail" | "fetch_task_detail" | "delete_task" | "archive_task" | "snooze_task" | "unsnooze_task" | "create_tag" | "delete_tag" | "update_charge_card" | "update_charge_card_name" | "update_charge_card_name_card_user" | "issue_charge_card" | "issue_charge_cards" | "notification_settings_saved" | "referral_invite_sent" | "notification_mark_as_read" | "mark_as_complete_schedule" | "cancel_journal_entry" | "settings_accounting_accounts_updated" | "create_card_setup" | "confirm_card_setup" | "add_card_payment_source" | "fetch_payment_sources" | "task_assigned_toast_notification" | "task_due_tomorrow_toast_notification" | "task_deleted_toast_notification" | "task_archived_toast_notification" | "task_overdue_toast_notification" | "task_overdue_toast_notification_creator" | "task_notification_count" | "task_activities_toast_notification" | "task_created_toast_notification" | "task_group_creation_success" | "task_group_deletion_success" | "task_group_update_success" | "primary_funding_account_updated" | "task_time_spent_validation" | "missing_receipts_attachment" | "flux_analysis_unreviewed" | "flux_analysis_reviewed" | "receipt_match" | "receipts_upload" | "receipts_bulk_match" | "billing_address_view" | "express_pay_submit" | "exclude_transaction" | "reconcile" | "save_reconcile_for_later" | "recon_transaction_categorize" | "recon_transaction_match" | "update_debit_card_pin_attempt" | "set_debit_card_pin" | "ai_cfo_create_session_and_submit" | "ai_cfo_chat_session_deleted" | "charge_card_auto_pay_enable" | "charge_card_auto_pay_disable" | "treasury_setup" | "treasury_accept_term" | "treasury_transfer_money" | "treasury_transfer_money_failed" | "auto_transfer_rule_create" | "auto_transfer_rule_update" | "auto_transfer_rule_delete" | "auto_transfer_rule_pause" | "auto_transfer_rule_resume" | "treasury_update_portfolio_allocation" | "send_email_magic_link_to_user" | "complete_profile_done" | "create_transfer_entry" | "reports_resync" | "invalid_phone_number" | "transactions_categorized_updated_failed" | "transaction_categorized_updated_failed" | "transactionsCategorized_transactionsUpdated" | "transactionsCategorized_transactionUpdated" | "transactionCategorized_transactionsUpdated" | "transactionCategorized_transactionUpdated" | "transactionsCategorized_transactionsFailed" | "transactionsCategorized_transactionFailed" | "transactionCategorized_transactionsFailed" | "transactionCategorized_transactionFailed" | "transactionsUpdated_transactionsFailed" | "transactionsUpdated_transactionFailed" | "transactionUpdated_transactionsFailed" | "transactionUpdated_transactionFailed" | "transactionsCategorized_transactionsUpdated_transactionsFailed" | "transactionsCategorized_transactionsUpdated_transactionFailed" | "transactionsCategorized_transactionUpdated_transactionsFailed" | "transactionCategorized_transactionsUpdated_transactionsFailed" | "transactionsCategorized_transactionUpdated_transactionFailed" | "transactionCategorized_transactionsUpdated_transactionFailed" | "transactionCategorized_transactionUpdated_transactionsFailed" | "transactionCategorized_transactionUpdated_transactionFailed" | "accounting_classes_enabled_update" | "capitalization_threshold_update" | "account_excluded_from_reconciliation" | "account_included_in_reconciliation" | "statement_parsing_in_progress" | "statement_parsing_complete" | "ai_accountant_trigger_job" | "oauth_consent_approve" | "oauth_consent_invalid_request" | "credit_agent_update_profile" | "credit_agent_save_macro" | "credit_agent_schedule_cron" | "create_sub_task" | "qbo_connection_already_linked" | "qbo_connection_failed";
|
|
2
2
|
export type SnackbarMessageSections = ReturnType<typeof toSnackbarMessageSection>;
|
|
3
3
|
export declare const toSnackbarMessageSectionTexts: (v: string) => "notification" | "failed" | "success";
|
|
4
4
|
export type SnackbarMessageSectionTexts = ReturnType<typeof toSnackbarMessageSectionTexts>;
|
|
@@ -226,6 +226,7 @@ const ALL_SNACKBAR_MESSAGE_SECTIONS = [
|
|
|
226
226
|
'credit_agent_schedule_cron',
|
|
227
227
|
'create_sub_task',
|
|
228
228
|
'qbo_connection_already_linked',
|
|
229
|
+
'qbo_connection_failed',
|
|
229
230
|
];
|
|
230
231
|
const toSnackbarMessageSection = (v) => (0, stringToUnion_1.stringToUnion)(v, ALL_SNACKBAR_MESSAGE_SECTIONS);
|
|
231
232
|
exports.toSnackbarMessageSection = toSnackbarMessageSection;
|
|
@@ -11,10 +11,9 @@ const responsePayload_1 = require("../../../responsePayload");
|
|
|
11
11
|
const companyReducer_1 = require("../../company/companyReducer");
|
|
12
12
|
const snackbarReducer_1 = require("../../snackbar/snackbarReducer");
|
|
13
13
|
const tenantReducer_1 = require("../tenantReducer");
|
|
14
|
-
// Backend returns 409
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// the generic save-connection failure UI.
|
|
14
|
+
// Backend returns 409 when the QBO realm is already linked to another active
|
|
15
|
+
// tenant. That case gets specific rejection copy; every other connect failure
|
|
16
|
+
// falls back to the generic connect-failure snackbar.
|
|
18
17
|
const QBO_REALM_ALREADY_LINKED_STATUS = 409;
|
|
19
18
|
const saveExternalConnectionEpic = (actions$, _, zeniAPI) => actions$.pipe((0, operators_1.filter)(tenantReducer_1.saveExternalConnection.match), (0, operators_1.switchMap)((action) => zeniAPI
|
|
20
19
|
.putAndGetJSON(`${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/connections/${action.payload.connectionMeta.connection_id}`, {
|
|
@@ -43,13 +42,13 @@ const saveExternalConnectionEpic = (actions$, _, zeniAPI) => actions$.pipe((0, o
|
|
|
43
42
|
}
|
|
44
43
|
else {
|
|
45
44
|
actions.push((0, tenantReducer_1.saveExternalConnectionFailure)(action.payload.tenantId, response.status));
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
45
|
+
actions.push((0, snackbarReducer_1.openSnackbar)({
|
|
46
|
+
messageSection: response.status?.code === QBO_REALM_ALREADY_LINKED_STATUS
|
|
47
|
+
? 'qbo_connection_already_linked'
|
|
48
|
+
: 'qbo_connection_failed',
|
|
49
|
+
messageText: 'failed',
|
|
50
|
+
type: 'error',
|
|
51
|
+
}));
|
|
53
52
|
if (action.payload.companyId != null &&
|
|
54
53
|
action.payload.userId != null) {
|
|
55
54
|
actions.push((0, companyReducer_1.updateQBOConnectionStatusForCompany)({
|
|
@@ -65,6 +64,11 @@ const saveExternalConnectionEpic = (actions$, _, zeniAPI) => actions$.pipe((0, o
|
|
|
65
64
|
const actions = [];
|
|
66
65
|
actions.push((0, tenantReducer_1.saveExternalConnectionFailure)(action.payload.tenantId, (0, responsePayload_1.createZeniAPIStatus)('Unexpected error', 'Save Connection REST API call errored out' +
|
|
67
66
|
JSON.stringify(error))));
|
|
67
|
+
actions.push((0, snackbarReducer_1.openSnackbar)({
|
|
68
|
+
messageSection: 'qbo_connection_failed',
|
|
69
|
+
messageText: 'failed',
|
|
70
|
+
type: 'error',
|
|
71
|
+
}));
|
|
68
72
|
if (action.payload.companyId != null &&
|
|
69
73
|
action.payload.userId != null) {
|
|
70
74
|
actions.push((0, companyReducer_1.updateQBOConnectionStatusForCompany)({
|
|
@@ -223,6 +223,7 @@ const ALL_SNACKBAR_MESSAGE_SECTIONS = [
|
|
|
223
223
|
'credit_agent_schedule_cron',
|
|
224
224
|
'create_sub_task',
|
|
225
225
|
'qbo_connection_already_linked',
|
|
226
|
+
'qbo_connection_failed',
|
|
226
227
|
];
|
|
227
228
|
export const toSnackbarMessageSection = (v) => stringToUnion(v, ALL_SNACKBAR_MESSAGE_SECTIONS);
|
|
228
229
|
// Keeping this different from the message type, since we may not have messages in all instances
|
|
@@ -5,10 +5,9 @@ import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload
|
|
|
5
5
|
import { updateQBOConnectionStatusForCompany } from '../../company/companyReducer';
|
|
6
6
|
import { openSnackbar } from '../../snackbar/snackbarReducer';
|
|
7
7
|
import { saveExternalConnection, saveExternalConnectionFailure, saveExternalConnectionSuccess, } from '../tenantReducer';
|
|
8
|
-
// Backend returns 409
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// the generic save-connection failure UI.
|
|
8
|
+
// Backend returns 409 when the QBO realm is already linked to another active
|
|
9
|
+
// tenant. That case gets specific rejection copy; every other connect failure
|
|
10
|
+
// falls back to the generic connect-failure snackbar.
|
|
12
11
|
const QBO_REALM_ALREADY_LINKED_STATUS = 409;
|
|
13
12
|
export const saveExternalConnectionEpic = (actions$, _, zeniAPI) => actions$.pipe(filter(saveExternalConnection.match), switchMap((action) => zeniAPI
|
|
14
13
|
.putAndGetJSON(`${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/connections/${action.payload.connectionMeta.connection_id}`, {
|
|
@@ -37,13 +36,13 @@ export const saveExternalConnectionEpic = (actions$, _, zeniAPI) => actions$.pip
|
|
|
37
36
|
}
|
|
38
37
|
else {
|
|
39
38
|
actions.push(saveExternalConnectionFailure(action.payload.tenantId, response.status));
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
39
|
+
actions.push(openSnackbar({
|
|
40
|
+
messageSection: response.status?.code === QBO_REALM_ALREADY_LINKED_STATUS
|
|
41
|
+
? 'qbo_connection_already_linked'
|
|
42
|
+
: 'qbo_connection_failed',
|
|
43
|
+
messageText: 'failed',
|
|
44
|
+
type: 'error',
|
|
45
|
+
}));
|
|
47
46
|
if (action.payload.companyId != null &&
|
|
48
47
|
action.payload.userId != null) {
|
|
49
48
|
actions.push(updateQBOConnectionStatusForCompany({
|
|
@@ -59,6 +58,11 @@ export const saveExternalConnectionEpic = (actions$, _, zeniAPI) => actions$.pip
|
|
|
59
58
|
const actions = [];
|
|
60
59
|
actions.push(saveExternalConnectionFailure(action.payload.tenantId, createZeniAPIStatus('Unexpected error', 'Save Connection REST API call errored out' +
|
|
61
60
|
JSON.stringify(error))));
|
|
61
|
+
actions.push(openSnackbar({
|
|
62
|
+
messageSection: 'qbo_connection_failed',
|
|
63
|
+
messageText: 'failed',
|
|
64
|
+
type: 'error',
|
|
65
|
+
}));
|
|
62
66
|
if (action.payload.companyId != null &&
|
|
63
67
|
action.payload.userId != null) {
|
|
64
68
|
actions.push(updateQBOConnectionStatusForCompany({
|
package/lib/esm/index.js
CHANGED
|
@@ -395,9 +395,9 @@ import { getAllTopExpenses, getTop6Expenses, getTopExpenses, } from './view/topE
|
|
|
395
395
|
*/
|
|
396
396
|
import { TOP_EX_TIME_PERIODS, } from './view/topEx/topExState';
|
|
397
397
|
import { getAccountingProviderAttachment, } from './view/transactionDetail/getAccountingProviderAttachmentSelector';
|
|
398
|
-
import { JOURNAL_ENTRY_SORT_KEYS, filterJournalEntryLinesExcludingLinked, journalEntryDefaultSortConfig, journalEntryTotalsByPostingSide, sortJournalEntryLines, sumJournalEntryAmounts, toJournalEntrySortKey, } from './view/transactionDetail/journalEntryLinesViewModel';
|
|
398
|
+
import { JOURNAL_ENTRY_SORT_KEYS, filterJournalEntryLinesExcludingLinked, journalEntryDefaultSortConfig, journalEntrySplitReconciliation, journalEntryTotalsByPostingSide, sortJournalEntryLines, sumJournalEntryAmounts, toJournalEntrySortKey, } from './view/transactionDetail/journalEntryLinesViewModel';
|
|
399
399
|
import { getChangedLineItemCountFromLocalData, isAccountUncategorized, } from './view/transactionDetail/transactionDetailLocalDataHelper';
|
|
400
|
-
import { clearTransactionDetailSaveStatus, deleteTransactionAttachment, downloadAccountingProviderAttachment, downloadAccountingProviderAttachmentFailure, downloadAccountingProviderAttachmentSuccess, fetchTransactionDetail, initializeTransactionDetailLocalData, markCategoryClassRecommendationsFailureForTransactionDetail, removeEntityRecommendationForLineIdForTransactionDetail, resetAllLineItemsToVendor, resetLineItemsCategoryClassInLocalData, resetSelectedCustomer, resetVendor, saveTransactionDetail, saveTransactionDetailLocalData, setAllLineItemsLocalDataIsUpdateAllChecked, setAllLineItemsToCategoryClassInLocalData, setIsVendorUpdateAllChecked, updateLatestSelectedEntity, updateLocalDataWithTransactionsUpdateWithVendorCheckbox, updateSelectedCustomer, updateSelectedVendor, updateTransactionDetail, updateVendorBulkRecommendationsFetchStatus, updateVendorToAllLineItems, uploadMissingAttachmentSuccess, } from './view/transactionDetail/transactionDetailReducer';
|
|
400
|
+
import { addSplitLine, clearTransactionDetailSaveStatus, deleteSplitLine, deleteTransactionAttachment, downloadAccountingProviderAttachment, downloadAccountingProviderAttachmentFailure, downloadAccountingProviderAttachmentSuccess, fetchTransactionDetail, initializeTransactionDetailLocalData, markCategoryClassRecommendationsFailureForTransactionDetail, removeEntityRecommendationForLineIdForTransactionDetail, resetAllLineItemsToVendor, resetLineItemsCategoryClassInLocalData, resetSelectedCustomer, resetVendor, saveTransactionDetail, saveTransactionDetailLocalData, setAllLineItemsLocalDataIsUpdateAllChecked, setAllLineItemsToCategoryClassInLocalData, setIsVendorUpdateAllChecked, toggleSplitEditMode, updateLatestSelectedEntity, updateLocalDataWithTransactionsUpdateWithVendorCheckbox, updateSelectedCustomer, updateSelectedVendor, updateTransactionDetail, updateVendorBulkRecommendationsFetchStatus, updateVendorToAllLineItems, uploadMissingAttachmentSuccess, } from './view/transactionDetail/transactionDetailReducer';
|
|
401
401
|
import { getInitialUpdatablePeriodsForTransactionRecommendations, getTransactionDetail, getTransactionDetailForTransaction, } from './view/transactionDetail/transactionDetailSelector';
|
|
402
402
|
import { getThirdPartyIdFromTransactionId, } from './view/transactionDetail/transactionDetailState';
|
|
403
403
|
import { getTransactionListByAccount, getTransactionListUIStateByAccountKey, } from './view/transactionList/transactionListByAccountSelector';
|
|
@@ -516,7 +516,7 @@ export { isTransferTransaction };
|
|
|
516
516
|
export { getSupportedTransactionById, };
|
|
517
517
|
export { clearTransactionList, updateLatestTransactionId, parallelFetchAccountTransactionList, parallelFetchEntityTransactionList, fetchAccountTransactionList, updateTransactionListByAccount, updateTransactionListByAccountFailure, updateTransactionListByAccountUIState, parallelFetchClassTransactionList, updateTransactionListByClassUIState, updateTransactionListByClass, parallelFetchProjectTransactionList, updateTransactionListByProjectUIState, updateTransactionListByProject, updateTransactionListUIStateByCategoryType, parallelFetchTransactionListByCategoryType, };
|
|
518
518
|
export { getTransactionListByAccount, getTransactionListUIStateByAccountKey, };
|
|
519
|
-
export { fetchEntityTransactionList, updateTransactionListByEntity, saveTransactionDetailLocalData, clearTransactionDetailSaveStatus, updateTransactionListByEntityFailure, resetLineItemsCategoryClassInLocalData, setAllLineItemsToCategoryClassInLocalData, saveTransactionDetail, setAllLineItemsLocalDataIsUpdateAllChecked, initializeTransactionDetailLocalData, markCategoryClassRecommendationsFailureForTransactionDetail, removeEntityRecommendationForLineIdForTransactionDetail, updateSelectedVendor, updateSelectedCustomer, resetSelectedCustomer, resetVendor, updateLocalDataWithTransactionsUpdateWithVendorCheckbox, updateVendorToAllLineItems, resetAllLineItemsToVendor, updateLatestSelectedEntity, updateVendorBulkRecommendationsFetchStatus, setIsVendorUpdateAllChecked, };
|
|
519
|
+
export { fetchEntityTransactionList, updateTransactionListByEntity, saveTransactionDetailLocalData, clearTransactionDetailSaveStatus, updateTransactionListByEntityFailure, resetLineItemsCategoryClassInLocalData, setAllLineItemsToCategoryClassInLocalData, saveTransactionDetail, setAllLineItemsLocalDataIsUpdateAllChecked, initializeTransactionDetailLocalData, markCategoryClassRecommendationsFailureForTransactionDetail, removeEntityRecommendationForLineIdForTransactionDetail, updateSelectedVendor, updateSelectedCustomer, resetSelectedCustomer, resetVendor, updateLocalDataWithTransactionsUpdateWithVendorCheckbox, updateVendorToAllLineItems, resetAllLineItemsToVendor, updateLatestSelectedEntity, updateVendorBulkRecommendationsFetchStatus, setIsVendorUpdateAllChecked, addSplitLine, deleteSplitLine, toggleSplitEditMode, };
|
|
520
520
|
export { downloadAccountingProviderAttachment, downloadAccountingProviderAttachmentSuccess, downloadAccountingProviderAttachmentFailure, fetchTransactionDetail, updateTransactionDetail, uploadMissingAttachmentSuccess, deleteTransactionAttachment, getThirdPartyIdFromTransactionId, };
|
|
521
521
|
export { getTransactionListByEntity };
|
|
522
522
|
export { getTransactionListByClass, getTransactionListUIStateByClassKey, getTransactionListByProject, getTransactionListUIStateByProjectKey, getTransactionListByEntityForSinglePeriod, };
|
|
@@ -595,7 +595,7 @@ export { getNextNthWorkingDay, getNextTransferDate, getPreviousNthWorkingDay, is
|
|
|
595
595
|
export { fetchCompanyOnboardingView, fetchQBOConnectionPool, updateQBOConnectionPoolExternalConnection, fetchOnboardingCompletedCompanies, saveOnboardingCustomerCompletedStatus, toProductType, toProductTypeStrict, getOnboardingCockpitView, getNewOnboardingCustomerView, getOnboardingEmailGroup, initializeOnboardingCustomerViewUpdateData, clearOnboardingCustomerViewUpdateData, saveOnboardingCustomerViewUpdateData, updateOnboardingCustomerListUIState, updateCustomerCreationStatus, updateStatusAfterOnboardingCompleted, saveOnboardingCustomerViewUpdates, saveOnboardingCustomerNotes, saveOnboardingCustomerDataInLocalStore, updateOnboardingCustomerDataInLocalStore, resetNewOnboardedCustomerId, sendOnboardingCustomerViewInvite, retryBankAccountConnectionForOnboarding, };
|
|
596
596
|
export { getTransactionsListByCategoryType, getTransactionListUIStateByCategoryType, };
|
|
597
597
|
export { getChangedLineItemCountFromLocalData, isAccountUncategorized };
|
|
598
|
-
export { JOURNAL_ENTRY_SORT_KEYS, filterJournalEntryLinesExcludingLinked, journalEntryDefaultSortConfig, journalEntryTotalsByPostingSide, sortJournalEntryLines, sumJournalEntryAmounts, toJournalEntrySortKey, };
|
|
598
|
+
export { JOURNAL_ENTRY_SORT_KEYS, filterJournalEntryLinesExcludingLinked, journalEntryDefaultSortConfig, journalEntryTotalsByPostingSide, journalEntrySplitReconciliation, sortJournalEntryLines, sumJournalEntryAmounts, toJournalEntrySortKey, };
|
|
599
599
|
export { openSnackbar, closeSnackbar, getSnackbar, };
|
|
600
600
|
export { getSpendManagementEffectiveListPeriod, getSelectedCompanyOfficer, showBillPayPromoPage, showReimbursementPromoPage, getActualPaymentDate, isVerifiedStatusCode, isVerifiedPaymentAccountProviderStatusCode, isBulkProcessing, getBillListUniqueType, getRemiListUniqueType, getBulkOperationSuffix, toAccountsPromoConfig, toSameDayAchDisablementConfig, isPaymentMethodOutsideZeni, isAwaitingMarkAsPaid, checkIfLowBalance, };
|
|
601
601
|
export { canSendMonthEndEmailReport, shouldEnableCalendarPickerForLastReportSent, };
|
|
@@ -168,6 +168,13 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
168
168
|
if (searchString !== undefined) {
|
|
169
169
|
draft.transactionCategorizationView[selectedTab].uiState.searchString =
|
|
170
170
|
searchString;
|
|
171
|
+
// When search is cleared, drop all sub-tab snapshots. A snapshot
|
|
172
|
+
// captured while search was active holds filtered row IDs — restoring
|
|
173
|
+
// it after the clear would show stale search results instead of the
|
|
174
|
+
// full unfiltered list.
|
|
175
|
+
if (searchString === '' && selectedTab === 'autoCategorized') {
|
|
176
|
+
draft.autoCategorizedSubTabCache = {};
|
|
177
|
+
}
|
|
171
178
|
}
|
|
172
179
|
draft.transactionCategorizationView[selectedTab].error = undefined;
|
|
173
180
|
const monthYearPeriod = {
|
|
@@ -45,7 +45,15 @@ export const toTransactionUpdatesFromTransactionDetailLocalData = (transactionDe
|
|
|
45
45
|
let lastUpdatedCategory;
|
|
46
46
|
let lastUpdatedClass;
|
|
47
47
|
// single recommendation to just pick the last recommendation in the list of lines
|
|
48
|
+
const existingLineIds = new Set(transaction.lines?.map((l) => l.id) ?? []);
|
|
49
|
+
const deletedLineIds = new Set((transactionDetailLocalData.deletedLineIds ?? []).filter((id) => existingLineIds.has(id)));
|
|
50
|
+
// Collect new lines — in lineItemById but not yet in transaction.lines.
|
|
51
|
+
const addedLines = Object.values(transactionDetailLocalData.lineItemById).filter((item) => !existingLineIds.has(item.id));
|
|
48
52
|
transaction.lines?.forEach((line) => {
|
|
53
|
+
// Skip lines the user deleted in edit mode.
|
|
54
|
+
if (deletedLineIds.has(line.id)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
49
57
|
const lineItem = transactionDetailLocalData.lineItemById[line.id];
|
|
50
58
|
if (line.type === 'transaction_with_account_and_class_line' ||
|
|
51
59
|
line.type === 'transaction_with_product_or_service_line' ||
|
|
@@ -56,6 +64,7 @@ export const toTransactionUpdatesFromTransactionDetailLocalData = (transactionDe
|
|
|
56
64
|
const entityDetails = getEntityDetails(selectedEntity, lineItem, transactionDetailLocalData.vendorUpdates, transactionDetailLocalData.customerUpdates);
|
|
57
65
|
updatesByLineId[line.id] = {
|
|
58
66
|
lineDescription: lineItem.lineDescription,
|
|
67
|
+
amount: lineItem.amount,
|
|
59
68
|
class: lineItem.class,
|
|
60
69
|
account: lineItem.account,
|
|
61
70
|
// Project is at the transaction level for invoice/credit_memo; omit from lines.
|
|
@@ -77,6 +86,17 @@ export const toTransactionUpdatesFromTransactionDetailLocalData = (transactionDe
|
|
|
77
86
|
currentSelectedEntityLine.type === 'vendor'
|
|
78
87
|
? lineItem.vendor
|
|
79
88
|
: undefined,
|
|
89
|
+
// Carry the JE debit/credit side so a split edit that flips an
|
|
90
|
+
// existing line's posting side persists through the line patch.
|
|
91
|
+
...(line.type === 'journal_entry_transaction_line'
|
|
92
|
+
? {
|
|
93
|
+
postingType: lineItem.postingType,
|
|
94
|
+
postingTypeName: lineItem.postingType != null
|
|
95
|
+
? lineItem.postingType.charAt(0).toUpperCase() +
|
|
96
|
+
lineItem.postingType.slice(1)
|
|
97
|
+
: lineItem.postingTypeName,
|
|
98
|
+
}
|
|
99
|
+
: {}),
|
|
80
100
|
};
|
|
81
101
|
}
|
|
82
102
|
let hasCategoryChanged = false;
|
|
@@ -163,6 +183,8 @@ export const toTransactionUpdatesFromTransactionDetailLocalData = (transactionDe
|
|
|
163
183
|
and all the line items are NOT uncategorized*/
|
|
164
184
|
markAsMiscatUpdates: isTransactionMisCategorized,
|
|
165
185
|
anyBulkUpdates: isBulkUpdate,
|
|
186
|
+
addedLines: addedLines.length > 0 ? addedLines : undefined,
|
|
187
|
+
deletedLineIds: deletedLineIds.size > 0 ? [...deletedLineIds] : undefined,
|
|
166
188
|
};
|
|
167
189
|
};
|
|
168
190
|
const doUpdateEntityUpdates = (entityUpdates, entityId, entityType, recommendation, isBulkUpdate = false, updatedEntityId, entityName) => {
|
|
@@ -58,7 +58,9 @@ export const updateTransactionDetailEpic = (actions$, state$, zeniAPI) => action
|
|
|
58
58
|
if (response.data.vendors != null) {
|
|
59
59
|
actions.push(updateVendors(response.data.vendors, 'merge'));
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
// 'replace' so deleted lines are removed from Redux; 'merge' would
|
|
62
|
+
// keep any line absent from the server response (deletion bug).
|
|
63
|
+
actions.push(updateTransaction(transactionDetailKeyForResponse, response.data.transaction, 'replace'));
|
|
62
64
|
actions.push(initializeTransactionDetailLocalData({
|
|
63
65
|
transactionId: transactionIdResp,
|
|
64
66
|
vendorId: response.data.vendors?.[0]?.vendor_id,
|
|
@@ -159,7 +161,7 @@ function preparePayload(transactionState, vendorState, transactionDetailState, t
|
|
|
159
161
|
};
|
|
160
162
|
}
|
|
161
163
|
if (detail != null) {
|
|
162
|
-
const transUpdates = toTransactionUpdatePayload(saveUpdates.updates, detail, cotTransactionTracking, isAccountingProjectsEnabled, vendorUpdates, customerUpdates, currentSelectedEntity);
|
|
164
|
+
const transUpdates = toTransactionUpdatePayload(saveUpdates.updates, detail, cotTransactionTracking, isAccountingProjectsEnabled, vendorUpdates, customerUpdates, currentSelectedEntity, saveUpdates.deletedLineIds, saveUpdates.addedLines);
|
|
163
165
|
const entityUpdates = toEntityUpdatePayload(saveUpdates.entityRelatedUpdates);
|
|
164
166
|
return {
|
|
165
167
|
entity_related_updates: entityUpdates,
|
|
@@ -169,7 +171,7 @@ function preparePayload(transactionState, vendorState, transactionDetailState, t
|
|
|
169
171
|
}
|
|
170
172
|
return undefined;
|
|
171
173
|
}
|
|
172
|
-
export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracking, isAccountingProjectsEnabled, vendorUpdates, customerUpdates, tranSelectedEntity) => {
|
|
174
|
+
export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracking, isAccountingProjectsEnabled, vendorUpdates, customerUpdates, tranSelectedEntity, deletedLineIds, addedLines) => {
|
|
173
175
|
const transaction = Object.assign({}, detail);
|
|
174
176
|
transaction.memo = updates.memo ?? '';
|
|
175
177
|
// Unlike `memo` (always reset to '' when absent), `documentId` falls back to
|
|
@@ -178,6 +180,11 @@ export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracki
|
|
|
178
180
|
// either the unchanged current value or undefined, and we must not send an
|
|
179
181
|
// empty `document_id` that the server would treat as clearing the number.
|
|
180
182
|
transaction.documentId = updates.documentId ?? transaction.documentId;
|
|
183
|
+
// Remove lines deleted by the user in edit mode before patching or serializing.
|
|
184
|
+
if (deletedLineIds != null && deletedLineIds.length > 0) {
|
|
185
|
+
const deletedSet = new Set(deletedLineIds);
|
|
186
|
+
transaction.lines = transaction.lines?.filter((line) => !deletedSet.has(line.id));
|
|
187
|
+
}
|
|
181
188
|
const recommendationUpdates = {};
|
|
182
189
|
for (const key in updates.updatesByLineId) {
|
|
183
190
|
const lineUpdates = updates.updatesByLineId[key];
|
|
@@ -185,6 +192,9 @@ export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracki
|
|
|
185
192
|
if (line.id === key) {
|
|
186
193
|
const lineTobeUpdated = Object.assign({}, line);
|
|
187
194
|
lineTobeUpdated.lineDescription = lineUpdates.lineDescription ?? '';
|
|
195
|
+
if (lineUpdates.amount != null) {
|
|
196
|
+
lineTobeUpdated.amount = lineUpdates.amount;
|
|
197
|
+
}
|
|
188
198
|
if (lineUpdates.class != null) {
|
|
189
199
|
lineTobeUpdated.class = lineUpdates.class;
|
|
190
200
|
}
|
|
@@ -206,6 +216,16 @@ export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracki
|
|
|
206
216
|
else {
|
|
207
217
|
lineTobeUpdated.vendor = undefined;
|
|
208
218
|
}
|
|
219
|
+
// Persist a debit↔credit flip on an existing JE line. `lineTobeUpdated`
|
|
220
|
+
// is typed as the account-and-class shape, but for a JE the runtime
|
|
221
|
+
// object is a JournalEntryTransactionLine, so cast to set the side.
|
|
222
|
+
if (lineUpdates.postingType != null) {
|
|
223
|
+
const journalEntryLine = lineTobeUpdated;
|
|
224
|
+
journalEntryLine.postingType = lineUpdates.postingType;
|
|
225
|
+
if (lineUpdates.postingTypeName != null) {
|
|
226
|
+
journalEntryLine.postingTypeName = lineUpdates.postingTypeName;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
209
229
|
if (lineUpdates.recommendation != null) {
|
|
210
230
|
recommendationUpdates[line.id] = lineUpdates.recommendation;
|
|
211
231
|
}
|
|
@@ -225,6 +245,12 @@ export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracki
|
|
|
225
245
|
});
|
|
226
246
|
transaction.lines = newLineMap;
|
|
227
247
|
}
|
|
248
|
+
// Append lines added by the user in edit mode. Sending id='' tells the
|
|
249
|
+
// backend to create a new QBO line rather than updating an existing one.
|
|
250
|
+
if (addedLines != null && addedLines.length > 0) {
|
|
251
|
+
const newLines = addedLines.map(toNewTransactionLine);
|
|
252
|
+
transaction.lines = [...(transaction.lines ?? []), ...newLines];
|
|
253
|
+
}
|
|
228
254
|
transaction.recommendations = recommendationUpdates;
|
|
229
255
|
// Convert transaction to payload first
|
|
230
256
|
let basePayload;
|
|
@@ -299,6 +325,52 @@ const checkIfVendorOrCustomerMissingForAnyLineItem = (transactionPayload) => {
|
|
|
299
325
|
}
|
|
300
326
|
return isAnyVendorOrCustomerMissing;
|
|
301
327
|
};
|
|
328
|
+
// Converts a locally-created line item (from edit mode) into the wire shape
|
|
329
|
+
// expected by the categorize endpoint. Sending id='' signals the backend to
|
|
330
|
+
// create a new QBO line rather than update an existing one.
|
|
331
|
+
//
|
|
332
|
+
// Type-dispatched so each transaction family contributes its own new-line
|
|
333
|
+
// shape: journal entries carry debit/credit `postingType`, expenses carry
|
|
334
|
+
// `billable`. Phase 3 types extend this by adding a branch rather than forking
|
|
335
|
+
// the function.
|
|
336
|
+
const EMPTY_ACCOUNT = {
|
|
337
|
+
accountDescription: '',
|
|
338
|
+
accountId: '',
|
|
339
|
+
accountName: '',
|
|
340
|
+
labels: [],
|
|
341
|
+
};
|
|
342
|
+
export const toNewTransactionLine = (item) => {
|
|
343
|
+
if (item.type === 'journal_entry_transaction_line') {
|
|
344
|
+
const journalEntryLine = {
|
|
345
|
+
...item,
|
|
346
|
+
id: '',
|
|
347
|
+
type: 'journal_entry_transaction_line',
|
|
348
|
+
platformLineDetailType: item.platformLineDetailType ?? 'journal_entry_line_detail',
|
|
349
|
+
account: item.account ?? EMPTY_ACCOUNT,
|
|
350
|
+
billable: item.billable ?? 'not_billable',
|
|
351
|
+
isCategoryMiscategorized: false,
|
|
352
|
+
isClassMiscategorized: false,
|
|
353
|
+
lineTransactionType: item.lineTransactionType ?? 'journal_entry',
|
|
354
|
+
postingType: item.postingType ?? 'debit',
|
|
355
|
+
postingTypeName: item.postingTypeName != null && item.postingTypeName !== ''
|
|
356
|
+
? item.postingTypeName
|
|
357
|
+
: (item.postingType ?? 'debit').charAt(0).toUpperCase() +
|
|
358
|
+
(item.postingType ?? 'debit').slice(1),
|
|
359
|
+
};
|
|
360
|
+
return journalEntryLine;
|
|
361
|
+
}
|
|
362
|
+
const accountAndClassLine = {
|
|
363
|
+
...item,
|
|
364
|
+
id: '',
|
|
365
|
+
type: 'transaction_with_account_and_class_line',
|
|
366
|
+
platformLineDetailType: item.platformLineDetailType ?? 'account_based_expense_line_detail',
|
|
367
|
+
account: item.account ?? EMPTY_ACCOUNT,
|
|
368
|
+
billable: item.billable ?? 'not_billable',
|
|
369
|
+
isCategoryMiscategorized: false,
|
|
370
|
+
isClassMiscategorized: false,
|
|
371
|
+
};
|
|
372
|
+
return accountAndClassLine;
|
|
373
|
+
};
|
|
302
374
|
const toEntityUpdatePayload = (entityUpdates) => {
|
|
303
375
|
const entityUpdatesPayload = [];
|
|
304
376
|
let entityKey;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Big from 'big.js';
|
|
1
2
|
import orderBy from 'lodash/orderBy';
|
|
2
3
|
import { stringToUnion } from '../../commonStateTypes/stringToUnion';
|
|
3
4
|
import { LINE_TYPE_LINKED, } from '../../entity/transaction/stateTypes/transactionLine';
|
|
@@ -101,6 +102,63 @@ export const journalEntryTotalsByPostingSide = (lines) => {
|
|
|
101
102
|
};
|
|
102
103
|
};
|
|
103
104
|
// ---------------------------------------------------------------------------
|
|
105
|
+
// Split reconciliation & validation (Phase 2 — JE split)
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Half-cent tolerance for the balance equality checks, matching the expense
|
|
108
|
+
// reconciliation row's epsilon. Sums use big.js so repeated decimal addition
|
|
109
|
+
// doesn't drift before the comparison.
|
|
110
|
+
const JOURNAL_ENTRY_BALANCE_EPSILON = 0.005;
|
|
111
|
+
/**
|
|
112
|
+
* Sums debit and credit sides (big.js-exact) and reports whether the split is
|
|
113
|
+
* balanced: total debits equal total credits AND equal the original
|
|
114
|
+
* transaction total. The original total of a valid JE equals its total debits,
|
|
115
|
+
* so it is passed in by the caller (`transaction.amount.amount`).
|
|
116
|
+
*/
|
|
117
|
+
export const journalEntrySplitReconciliation = (lines, originalTotal) => {
|
|
118
|
+
let debit = new Big(0);
|
|
119
|
+
let credit = new Big(0);
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
const amount = new Big(line.amount?.amount ?? 0);
|
|
122
|
+
if (line.postingType === 'debit') {
|
|
123
|
+
debit = debit.plus(amount);
|
|
124
|
+
}
|
|
125
|
+
else if (line.postingType === 'credit') {
|
|
126
|
+
credit = credit.plus(amount);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const balancedToEachOther = debit
|
|
130
|
+
.minus(credit)
|
|
131
|
+
.abs()
|
|
132
|
+
.lt(JOURNAL_ENTRY_BALANCE_EPSILON);
|
|
133
|
+
const balancedToOriginal = debit
|
|
134
|
+
.minus(originalTotal)
|
|
135
|
+
.abs()
|
|
136
|
+
.lt(JOURNAL_ENTRY_BALANCE_EPSILON);
|
|
137
|
+
return {
|
|
138
|
+
debitTotal: debit.toNumber(),
|
|
139
|
+
creditTotal: credit.toNumber(),
|
|
140
|
+
isBalanced: balancedToEachOther && balancedToOriginal,
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Save-time validation for a JE split (Figma `4165:33028` / `4165:45379`):
|
|
145
|
+
* 1. every line needs a non-zero debit or credit, and
|
|
146
|
+
* 2. total debits == total credits == original total.
|
|
147
|
+
* Returns the first failing rule's `errorKey` (maps to a strings entry). Does
|
|
148
|
+
* NOT require category/class to be chosen — balance is the only gate.
|
|
149
|
+
*/
|
|
150
|
+
export const validateJournalEntrySplit = (lines, originalTotal) => {
|
|
151
|
+
const everyLineHasSide = lines.every((line) => (line.postingType === 'debit' || line.postingType === 'credit') &&
|
|
152
|
+
Math.abs(line.amount?.amount ?? 0) > JOURNAL_ENTRY_BALANCE_EPSILON);
|
|
153
|
+
if (!everyLineHasSide) {
|
|
154
|
+
return { ok: false, errorKey: 'lineNeedsDebitOrCredit' };
|
|
155
|
+
}
|
|
156
|
+
if (!journalEntrySplitReconciliation(lines, originalTotal).isBalanced) {
|
|
157
|
+
return { ok: false, errorKey: 'totalsMustEqual' };
|
|
158
|
+
}
|
|
159
|
+
return { ok: true };
|
|
160
|
+
};
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
104
162
|
// Filter
|
|
105
163
|
// ---------------------------------------------------------------------------
|
|
106
164
|
/**
|
|
@@ -823,14 +823,16 @@ export const getChangedLineItemCountFromLocalData = (transaction, transactionLoc
|
|
|
823
823
|
line.type === 'transaction_with_product_or_service_line' ||
|
|
824
824
|
line.type === 'journal_entry_transaction_line') {
|
|
825
825
|
const accountLineLocal = transactionLocalData.lineItemById[lineId];
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
accountLineLocal.account?.
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
826
|
+
if (accountLineLocal != null) {
|
|
827
|
+
const accountLine = line;
|
|
828
|
+
hasCategoryChanged =
|
|
829
|
+
accountLineLocal.account?.qboId !== accountLine.account.qboId &&
|
|
830
|
+
accountLineLocal.account?.accountId !== accountLine.account.accountId;
|
|
831
|
+
hasClassChanged =
|
|
832
|
+
(accountLineLocal.class?.qboId != null ||
|
|
833
|
+
accountLine.class?.qboId != null) &&
|
|
834
|
+
accountLineLocal.class?.qboId !== accountLine.class?.qboId;
|
|
835
|
+
}
|
|
834
836
|
}
|
|
835
837
|
hasDescriptionChanged =
|
|
836
838
|
transactionLocalData.lineItemById[lineId] != null
|
|
@@ -663,6 +663,71 @@ const transactionDetail = createSlice({
|
|
|
663
663
|
};
|
|
664
664
|
},
|
|
665
665
|
},
|
|
666
|
+
// Split transaction — Phase 1 (expense)
|
|
667
|
+
addSplitLine: {
|
|
668
|
+
reducer(draft, action) {
|
|
669
|
+
const key = getTransactionDetailKey(action.payload.transactionId);
|
|
670
|
+
const detail = recordGet(draft.transactionDetailById, key, undefined);
|
|
671
|
+
if (detail != null) {
|
|
672
|
+
draft.transactionDetailById[key] = {
|
|
673
|
+
...detail,
|
|
674
|
+
transactionDetailLocalData: {
|
|
675
|
+
...detail.transactionDetailLocalData,
|
|
676
|
+
lineItemById: {
|
|
677
|
+
...detail.transactionDetailLocalData.lineItemById,
|
|
678
|
+
[action.payload.newLine.id]: action.payload.newLine,
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
prepare(transactionId, newLine) {
|
|
685
|
+
return { payload: { transactionId, newLine } };
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
deleteSplitLine: {
|
|
689
|
+
reducer(draft, action) {
|
|
690
|
+
const key = getTransactionDetailKey(action.payload.transactionId);
|
|
691
|
+
const detail = recordGet(draft.transactionDetailById, key, undefined);
|
|
692
|
+
if (detail == null) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const { lineItemById, deletedLineIds = [] } = detail.transactionDetailLocalData;
|
|
696
|
+
const updated = { ...lineItemById };
|
|
697
|
+
delete updated[action.payload.lineId];
|
|
698
|
+
draft.transactionDetailById[key] = {
|
|
699
|
+
...detail,
|
|
700
|
+
transactionDetailLocalData: {
|
|
701
|
+
...detail.transactionDetailLocalData,
|
|
702
|
+
lineItemById: updated,
|
|
703
|
+
deletedLineIds: [...deletedLineIds, action.payload.lineId],
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
},
|
|
707
|
+
prepare(transactionId, lineId) {
|
|
708
|
+
return { payload: { transactionId, lineId } };
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
toggleSplitEditMode: {
|
|
712
|
+
reducer(draft, action) {
|
|
713
|
+
const key = getTransactionDetailKey(action.payload.transactionId);
|
|
714
|
+
const detail = recordGet(draft.transactionDetailById, key, undefined);
|
|
715
|
+
if (detail != null) {
|
|
716
|
+
draft.transactionDetailById[key] = {
|
|
717
|
+
...detail,
|
|
718
|
+
transactionDetailLocalData: {
|
|
719
|
+
...detail.transactionDetailLocalData,
|
|
720
|
+
editMode: action.payload.editMode,
|
|
721
|
+
// Clear delete tracking when exiting edit mode without saving.
|
|
722
|
+
...(!action.payload.editMode && { deletedLineIds: [] }),
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
prepare(transactionId, editMode) {
|
|
728
|
+
return { payload: { transactionId, editMode } };
|
|
729
|
+
},
|
|
730
|
+
},
|
|
666
731
|
clearRecommendedVendorsList(draft) {
|
|
667
732
|
Object.assign(draft, initialState);
|
|
668
733
|
},
|
|
@@ -771,7 +836,7 @@ const transactionDetail = createSlice({
|
|
|
771
836
|
},
|
|
772
837
|
},
|
|
773
838
|
});
|
|
774
|
-
export const { downloadAccountingProviderAttachment, downloadAccountingProviderAttachmentSuccess, downloadAccountingProviderAttachmentFailure, fetchTransactionDetail, updateTransactionDetailFetchState, removeTransactionDetail, clearTransactionDetail, uploadMissingAttachmentSuccess, updateTransactionDetail, updateTransactionDetailSaveStatus, clearTransactionDetailSaveStatus, markTransactionAsNotMiscategorized, deleteTransactionAttachment, deleteTransactionAttachmentFailure, deleteTransactionAttachmentSuccess, updateStatusForTransactionNotMiscategorizedUpdate, updateStatusConsolidatedSaveTransactionDetail, updateTransactionStateIfUpdateActionNotAllowed, saveTransactionDetail, initializeTransactionDetailLocalData, saveTransactionDetailLocalData, clearTransactionDetailLocalData, resetLineItemsCategoryClassInLocalData, setAllLineItemsToCategoryClassInLocalData, setAllLineItemsLocalDataIsUpdateAllChecked, setEntityRecommendationForLineIdsForTransactionDetail, markCategoryClassRecommendationsInProgressForTransactionDetail, markCategoryClassRecommendationsCompletedForTransactionDetail, markCategoryClassRecommendationsFailureForTransactionDetail, removeEntityRecommendationForLineIdForTransactionDetail, updateSelectedVendor, updateSelectedCustomer, resetVendor, resetSelectedCustomer, updateLocalDataWithTransactionsUpdateWithVendorCheckbox, updateVendorToAllLineItems, resetAllLineItemsToVendor, updateLatestSelectedEntity, updateVendorBulkRecommendationsFetchStatus, setIsVendorUpdateAllChecked, } = transactionDetail.actions;
|
|
839
|
+
export const { downloadAccountingProviderAttachment, downloadAccountingProviderAttachmentSuccess, downloadAccountingProviderAttachmentFailure, fetchTransactionDetail, updateTransactionDetailFetchState, removeTransactionDetail, clearTransactionDetail, uploadMissingAttachmentSuccess, updateTransactionDetail, updateTransactionDetailSaveStatus, clearTransactionDetailSaveStatus, markTransactionAsNotMiscategorized, deleteTransactionAttachment, deleteTransactionAttachmentFailure, deleteTransactionAttachmentSuccess, updateStatusForTransactionNotMiscategorizedUpdate, updateStatusConsolidatedSaveTransactionDetail, updateTransactionStateIfUpdateActionNotAllowed, saveTransactionDetail, initializeTransactionDetailLocalData, saveTransactionDetailLocalData, clearTransactionDetailLocalData, resetLineItemsCategoryClassInLocalData, setAllLineItemsToCategoryClassInLocalData, setAllLineItemsLocalDataIsUpdateAllChecked, setEntityRecommendationForLineIdsForTransactionDetail, markCategoryClassRecommendationsInProgressForTransactionDetail, markCategoryClassRecommendationsCompletedForTransactionDetail, markCategoryClassRecommendationsFailureForTransactionDetail, removeEntityRecommendationForLineIdForTransactionDetail, updateSelectedVendor, updateSelectedCustomer, resetVendor, resetSelectedCustomer, updateLocalDataWithTransactionsUpdateWithVendorCheckbox, updateVendorToAllLineItems, resetAllLineItemsToVendor, updateLatestSelectedEntity, updateVendorBulkRecommendationsFetchStatus, setIsVendorUpdateAllChecked, addSplitLine, deleteSplitLine, toggleSplitEditMode, } = transactionDetail.actions;
|
|
775
840
|
export default transactionDetail.reducer;
|
|
776
841
|
function getUpdatedEntityLocalData(detail, updates, entity, resetRecommendations, type) {
|
|
777
842
|
const key = type === 'customer' ? 'customerUpdates' : 'vendorUpdates';
|
package/lib/index.d.ts
CHANGED
|
@@ -582,9 +582,9 @@ import { TopExAccount, TopExReport, TopExTimePeriodWithMetaData, TopExpenses } f
|
|
|
582
582
|
*/
|
|
583
583
|
import { TOP_EX_TIME_PERIODS, TopExTimePeriod, TopExpense } from './view/topEx/topExState';
|
|
584
584
|
import { AccountingProviderAttachmentSelector, getAccountingProviderAttachment } from './view/transactionDetail/getAccountingProviderAttachmentSelector';
|
|
585
|
-
import { JOURNAL_ENTRY_SORT_KEYS, JournalEntryRowSortConfig, JournalEntrySortAccessors, JournalEntrySortKey, filterJournalEntryLinesExcludingLinked, journalEntryDefaultSortConfig, journalEntryTotalsByPostingSide, sortJournalEntryLines, sumJournalEntryAmounts, toJournalEntrySortKey } from './view/transactionDetail/journalEntryLinesViewModel';
|
|
585
|
+
import { JOURNAL_ENTRY_SORT_KEYS, JournalEntryRowSortConfig, JournalEntrySortAccessors, JournalEntrySortKey, filterJournalEntryLinesExcludingLinked, journalEntryDefaultSortConfig, journalEntrySplitReconciliation, journalEntryTotalsByPostingSide, sortJournalEntryLines, sumJournalEntryAmounts, toJournalEntrySortKey } from './view/transactionDetail/journalEntryLinesViewModel';
|
|
586
586
|
import { getChangedLineItemCountFromLocalData, isAccountUncategorized } from './view/transactionDetail/transactionDetailLocalDataHelper';
|
|
587
|
-
import { clearTransactionDetailSaveStatus, deleteTransactionAttachment, downloadAccountingProviderAttachment, downloadAccountingProviderAttachmentFailure, downloadAccountingProviderAttachmentSuccess, fetchTransactionDetail, initializeTransactionDetailLocalData, markCategoryClassRecommendationsFailureForTransactionDetail, removeEntityRecommendationForLineIdForTransactionDetail, resetAllLineItemsToVendor, resetLineItemsCategoryClassInLocalData, resetSelectedCustomer, resetVendor, saveTransactionDetail, saveTransactionDetailLocalData, setAllLineItemsLocalDataIsUpdateAllChecked, setAllLineItemsToCategoryClassInLocalData, setIsVendorUpdateAllChecked, updateLatestSelectedEntity, updateLocalDataWithTransactionsUpdateWithVendorCheckbox, updateSelectedCustomer, updateSelectedVendor, updateTransactionDetail, updateVendorBulkRecommendationsFetchStatus, updateVendorToAllLineItems, uploadMissingAttachmentSuccess } from './view/transactionDetail/transactionDetailReducer';
|
|
587
|
+
import { addSplitLine, clearTransactionDetailSaveStatus, deleteSplitLine, deleteTransactionAttachment, downloadAccountingProviderAttachment, downloadAccountingProviderAttachmentFailure, downloadAccountingProviderAttachmentSuccess, fetchTransactionDetail, initializeTransactionDetailLocalData, markCategoryClassRecommendationsFailureForTransactionDetail, removeEntityRecommendationForLineIdForTransactionDetail, resetAllLineItemsToVendor, resetLineItemsCategoryClassInLocalData, resetSelectedCustomer, resetVendor, saveTransactionDetail, saveTransactionDetailLocalData, setAllLineItemsLocalDataIsUpdateAllChecked, setAllLineItemsToCategoryClassInLocalData, setIsVendorUpdateAllChecked, toggleSplitEditMode, updateLatestSelectedEntity, updateLocalDataWithTransactionsUpdateWithVendorCheckbox, updateSelectedCustomer, updateSelectedVendor, updateTransactionDetail, updateVendorBulkRecommendationsFetchStatus, updateVendorToAllLineItems, uploadMissingAttachmentSuccess } from './view/transactionDetail/transactionDetailReducer';
|
|
588
588
|
import { TransactionDetailReport, VendorDetailsView, getInitialUpdatablePeriodsForTransactionRecommendations, getTransactionDetail, getTransactionDetailForTransaction } from './view/transactionDetail/transactionDetailSelector';
|
|
589
589
|
import { TransactionUpdates, getThirdPartyIdFromTransactionId } from './view/transactionDetail/transactionDetailState';
|
|
590
590
|
import { TransactionDetailLineItemData, TransactionDetailLocalData, TransactionVendorUpdates } from './view/transactionDetail/transactionDetailTypes';
|
|
@@ -743,7 +743,7 @@ export { MatchedTransaction };
|
|
|
743
743
|
export { SupportedTransaction, SupportedTransactionWithCOT, SupportedTransactionPayload, getSupportedTransactionById, };
|
|
744
744
|
export { clearTransactionList, updateLatestTransactionId, parallelFetchAccountTransactionList, parallelFetchEntityTransactionList, fetchAccountTransactionList, updateTransactionListByAccount, updateTransactionListByAccountFailure, updateTransactionListByAccountUIState, parallelFetchClassTransactionList, updateTransactionListByClassUIState, updateTransactionListByClass, parallelFetchProjectTransactionList, updateTransactionListByProjectUIState, updateTransactionListByProject, updateTransactionListUIStateByCategoryType, parallelFetchTransactionListByCategoryType, };
|
|
745
745
|
export { TransactionListByAccountReport, getTransactionListByAccount, getTransactionListUIStateByAccountKey, };
|
|
746
|
-
export { fetchEntityTransactionList, updateTransactionListByEntity, saveTransactionDetailLocalData, clearTransactionDetailSaveStatus, updateTransactionListByEntityFailure, resetLineItemsCategoryClassInLocalData, setAllLineItemsToCategoryClassInLocalData, saveTransactionDetail, setAllLineItemsLocalDataIsUpdateAllChecked, initializeTransactionDetailLocalData, markCategoryClassRecommendationsFailureForTransactionDetail, removeEntityRecommendationForLineIdForTransactionDetail, updateSelectedVendor, updateSelectedCustomer, resetSelectedCustomer, resetVendor, updateLocalDataWithTransactionsUpdateWithVendorCheckbox, updateVendorToAllLineItems, resetAllLineItemsToVendor, updateLatestSelectedEntity, updateVendorBulkRecommendationsFetchStatus, setIsVendorUpdateAllChecked, };
|
|
746
|
+
export { fetchEntityTransactionList, updateTransactionListByEntity, saveTransactionDetailLocalData, clearTransactionDetailSaveStatus, updateTransactionListByEntityFailure, resetLineItemsCategoryClassInLocalData, setAllLineItemsToCategoryClassInLocalData, saveTransactionDetail, setAllLineItemsLocalDataIsUpdateAllChecked, initializeTransactionDetailLocalData, markCategoryClassRecommendationsFailureForTransactionDetail, removeEntityRecommendationForLineIdForTransactionDetail, updateSelectedVendor, updateSelectedCustomer, resetSelectedCustomer, resetVendor, updateLocalDataWithTransactionsUpdateWithVendorCheckbox, updateVendorToAllLineItems, resetAllLineItemsToVendor, updateLatestSelectedEntity, updateVendorBulkRecommendationsFetchStatus, setIsVendorUpdateAllChecked, addSplitLine, deleteSplitLine, toggleSplitEditMode, };
|
|
747
747
|
export { TransactionUpdates, downloadAccountingProviderAttachment, downloadAccountingProviderAttachmentSuccess, downloadAccountingProviderAttachmentFailure, fetchTransactionDetail, updateTransactionDetail, uploadMissingAttachmentSuccess, deleteTransactionAttachment, getThirdPartyIdFromTransactionId, };
|
|
748
748
|
export { TransactionListByEntityView, getTransactionListByEntity };
|
|
749
749
|
export { TransactionListByClassReport, getTransactionListByClass, getTransactionListUIStateByClassKey, TransactionListByProjectReport, getTransactionListByProject, getTransactionListUIStateByProjectKey, getTransactionListByEntityForSinglePeriod, };
|
|
@@ -843,7 +843,7 @@ export { fetchCompanyOnboardingView, fetchQBOConnectionPool, updateQBOConnection
|
|
|
843
843
|
export { TransactionListByCategoryType, getTransactionsListByCategoryType, getTransactionListUIStateByCategoryType, TransactionCategoryType, };
|
|
844
844
|
export { TransactionDetailLocalData, TransactionDetailLineItemData, TransactionVendorUpdates, };
|
|
845
845
|
export { getChangedLineItemCountFromLocalData, isAccountUncategorized };
|
|
846
|
-
export { JOURNAL_ENTRY_SORT_KEYS, filterJournalEntryLinesExcludingLinked, journalEntryDefaultSortConfig, journalEntryTotalsByPostingSide, sortJournalEntryLines, sumJournalEntryAmounts, toJournalEntrySortKey, };
|
|
846
|
+
export { JOURNAL_ENTRY_SORT_KEYS, filterJournalEntryLinesExcludingLinked, journalEntryDefaultSortConfig, journalEntryTotalsByPostingSide, journalEntrySplitReconciliation, sortJournalEntryLines, sumJournalEntryAmounts, toJournalEntrySortKey, };
|
|
847
847
|
export type { JournalEntryRowSortConfig, JournalEntrySortAccessors, JournalEntrySortKey, };
|
|
848
848
|
export { openSnackbar, closeSnackbar, getSnackbar, SnackbarMessageType, SnackbarMessageSections, SnackbarMessageSectionTexts, SnackbarMessageConfig, };
|
|
849
849
|
export { getSpendManagementEffectiveListPeriod, getSelectedCompanyOfficer, showBillPayPromoPage, showReimbursementPromoPage, getActualPaymentDate, isVerifiedStatusCode, isVerifiedPaymentAccountProviderStatusCode, isBulkProcessing, getBillListUniqueType, getRemiListUniqueType, BulkOperationType, BulkOperationFinishedType, getBulkOperationSuffix, ZeniAccountsPromoConfig, toAccountsPromoConfig, SameDayAchDisablementConfig, toSameDayAchDisablementConfig, isPaymentMethodOutsideZeni, isAwaitingMarkAsPaid, checkIfLowBalance, };
|