@zeniai/client-epic-state 5.2.86 → 5.2.87

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.
Files changed (71) hide show
  1. package/lib/entity/reconciliationReport/reconciliationReportPayload.d.ts +95 -0
  2. package/lib/entity/reconciliationReport/reconciliationReportPayload.js +2 -0
  3. package/lib/entity/reconciliationReport/reconciliationReportReducer.d.ts +7 -0
  4. package/lib/entity/reconciliationReport/reconciliationReportReducer.js +151 -0
  5. package/lib/entity/reconciliationReport/reconciliationReportSelector.d.ts +3 -0
  6. package/lib/entity/reconciliationReport/reconciliationReportSelector.js +10 -0
  7. package/lib/entity/reconciliationReport/reconciliationReportState.d.ts +107 -0
  8. package/lib/entity/reconciliationReport/reconciliationReportState.js +15 -0
  9. package/lib/epic.d.ts +2 -1
  10. package/lib/epic.js +2 -0
  11. package/lib/esm/entity/reconciliationReport/reconciliationReportPayload.js +1 -0
  12. package/lib/esm/entity/reconciliationReport/reconciliationReportReducer.js +146 -0
  13. package/lib/esm/entity/reconciliationReport/reconciliationReportSelector.js +4 -0
  14. package/lib/esm/entity/reconciliationReport/reconciliationReportState.js +10 -0
  15. package/lib/esm/epic.js +2 -0
  16. package/lib/esm/index.js +5 -2
  17. package/lib/esm/reducer.js +7 -1
  18. package/lib/esm/view/expenseAutomationView/selectors/reconciliationViewSelector.js +5 -1
  19. package/lib/esm/view/invoicing/createInvoice/epics/fetchCreateInvoiceFormPageEpic.js +19 -5
  20. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewPayload.js +57 -4
  21. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +3 -1
  22. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionDetailPageEpic.js +7 -4
  23. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionFormPageEpic.js +1 -0
  24. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/invoicingSubscriptionFormConfig.js +2 -0
  25. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +6 -2
  26. package/lib/esm/view/invoicing/invoicingCatalogListView/epics/fetchInvoicingCatalogPlanListEpic.js +14 -3
  27. package/lib/esm/view/invoicing/invoicingCatalogListView/epics/fetchInvoicingCatalogProductListEpic.js +14 -3
  28. package/lib/esm/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewReducer.js +2 -0
  29. package/lib/esm/view/invoicing/invoicingListTabCache.js +8 -0
  30. package/lib/esm/view/invoicing/issueCreditNote/issueCreditNotePayload.js +24 -9
  31. package/lib/esm/view/reconciliationReportView/epic/fetchReconciliationReportEpic.js +29 -0
  32. package/lib/esm/view/reconciliationReportView/reconciliationReportViewPayload.js +1 -0
  33. package/lib/esm/view/reconciliationReportView/reconciliationReportViewReducer.js +44 -0
  34. package/lib/esm/view/reconciliationReportView/reconciliationReportViewSelector.js +10 -0
  35. package/lib/esm/view/reconciliationReportView/reconciliationReportViewState.js +1 -0
  36. package/lib/index.d.ts +7 -3
  37. package/lib/index.js +52 -47
  38. package/lib/reducer.d.ts +2 -0
  39. package/lib/reducer.js +7 -1
  40. package/lib/rootStateTypes.d.ts +5 -1
  41. package/lib/view/expenseAutomationView/selectors/reconciliationViewSelector.d.ts +3 -0
  42. package/lib/view/expenseAutomationView/selectors/reconciliationViewSelector.js +5 -0
  43. package/lib/view/invoicing/createInvoice/epics/fetchCreateInvoiceFormPageEpic.js +18 -4
  44. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewPayload.d.ts +11 -1
  45. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewPayload.js +57 -4
  46. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.d.ts +2 -0
  47. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +2 -0
  48. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewState.d.ts +13 -0
  49. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionDetailPageEpic.js +6 -3
  50. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionFormPageEpic.js +1 -0
  51. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/invoicingSubscriptionFormConfig.d.ts +2 -0
  52. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/invoicingSubscriptionFormConfig.js +4 -1
  53. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +5 -1
  54. package/lib/view/invoicing/invoicingCatalogListView/epics/fetchInvoicingCatalogPlanListEpic.js +14 -3
  55. package/lib/view/invoicing/invoicingCatalogListView/epics/fetchInvoicingCatalogProductListEpic.js +14 -3
  56. package/lib/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewReducer.d.ts +6 -0
  57. package/lib/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewReducer.js +2 -0
  58. package/lib/view/invoicing/invoicingListTabCache.d.ts +11 -1
  59. package/lib/view/invoicing/invoicingListTabCache.js +10 -1
  60. package/lib/view/invoicing/issueCreditNote/issueCreditNotePayload.js +24 -9
  61. package/lib/view/reconciliationReportView/epic/fetchReconciliationReportEpic.d.ts +19 -0
  62. package/lib/view/reconciliationReportView/epic/fetchReconciliationReportEpic.js +33 -0
  63. package/lib/view/reconciliationReportView/reconciliationReportViewPayload.d.ts +3 -0
  64. package/lib/view/reconciliationReportView/reconciliationReportViewPayload.js +2 -0
  65. package/lib/view/reconciliationReportView/reconciliationReportViewReducer.d.ts +13 -0
  66. package/lib/view/reconciliationReportView/reconciliationReportViewReducer.js +48 -0
  67. package/lib/view/reconciliationReportView/reconciliationReportViewSelector.d.ts +9 -0
  68. package/lib/view/reconciliationReportView/reconciliationReportViewSelector.js +13 -0
  69. package/lib/view/reconciliationReportView/reconciliationReportViewState.d.ts +4 -0
  70. package/lib/view/reconciliationReportView/reconciliationReportViewState.js +2 -0
  71. package/package.json +1 -1
@@ -0,0 +1,146 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+ import { mapCurrencyPayloadToCurrency } from '../../commonPayloadTypes/v2/viewAndReportPayloadv2';
3
+ import { toAmountWC } from '../../commonStateTypes/amount';
4
+ import { clearAll } from '../../rootActions';
5
+ import { date } from '../../zeniDayJS';
6
+ import { toReconciliationStatusCodeType } from '../accountRecon/accountReconState';
7
+ import { toReconciliationReportDataSource, toReconciliationReportMatchType, } from './reconciliationReportState';
8
+ export const initialState = {
9
+ reportsByReconciliationId: {},
10
+ };
11
+ const reconciliationReport = createSlice({
12
+ name: 'reconciliationReport',
13
+ initialState,
14
+ reducers: {
15
+ updateReconciliationReport(draft, action) {
16
+ const report = mapReconciliationReportPayloadToReconciliationReport(action.payload);
17
+ draft.reportsByReconciliationId[report.reconciliationId] = report;
18
+ },
19
+ removeReconciliationReport(draft, action) {
20
+ delete draft.reportsByReconciliationId[action.payload];
21
+ },
22
+ },
23
+ extraReducers: (builder) => {
24
+ builder.addCase(clearAll, (draft) => {
25
+ Object.assign(draft, initialState);
26
+ });
27
+ },
28
+ });
29
+ export const { updateReconciliationReport, removeReconciliationReport } = reconciliationReport.actions;
30
+ export default reconciliationReport.reducer;
31
+ function toOptionalAmount(amount, currency) {
32
+ if (amount == null) {
33
+ return undefined;
34
+ }
35
+ return toAmountWC(amount, currency);
36
+ }
37
+ function toOptionalDate(value) {
38
+ if (value == null || value === '') {
39
+ return undefined;
40
+ }
41
+ return date(value);
42
+ }
43
+ function mapCountAmount(payload, currency) {
44
+ return {
45
+ count: payload.count,
46
+ amount: toAmountWC(payload.amount, currency),
47
+ };
48
+ }
49
+ function mapTransactionRow(payload, currency) {
50
+ return {
51
+ transactionId: payload.transaction_id ?? undefined,
52
+ date: toOptionalDate(payload.date),
53
+ type: payload.type,
54
+ payee: payload.payee,
55
+ match: toReconciliationReportMatchType(payload.match),
56
+ amount: toAmountWC(payload.amount, currency),
57
+ clearedBalance: payload.cleared_balance != null
58
+ ? toAmountWC(payload.cleared_balance, currency)
59
+ : undefined,
60
+ glAccount: payload.gl_account ?? undefined,
61
+ issueDate: toOptionalDate(payload.issue_date),
62
+ ageDays: payload.age_days ?? undefined,
63
+ };
64
+ }
65
+ function mapBucket(payload, currency) {
66
+ return {
67
+ count: payload.count,
68
+ amount: toAmountWC(payload.amount, currency),
69
+ transactions: payload.transactions.map((transaction) => mapTransactionRow(transaction, currency)),
70
+ };
71
+ }
72
+ function mapPaymentsDeposits(payload, currency) {
73
+ return {
74
+ payments: mapBucket(payload.payments, currency),
75
+ deposits: mapBucket(payload.deposits, currency),
76
+ };
77
+ }
78
+ export function mapReconciliationReportPayloadToReconciliationReport(payload) {
79
+ const currency = mapCurrencyPayloadToCurrency(payload.currency);
80
+ return {
81
+ reconciliationId: payload.reconciliation_id,
82
+ companyName: payload.company_name ?? undefined,
83
+ account: {
84
+ accountId: payload.account.account_id,
85
+ accountName: payload.account.account_name ?? undefined,
86
+ accountType: payload.account.account_type ?? undefined,
87
+ accountSubType: payload.account.account_sub_type ?? undefined,
88
+ last4Digits: payload.account.last_4_digits ?? undefined,
89
+ maskedAccountNumber: payload.account.masked_account_number ?? undefined,
90
+ financialInstitution: payload.account.financial_institution ?? undefined,
91
+ glAccountName: payload.account.gl_account_name ?? undefined,
92
+ coaId: payload.account.coa_id ?? undefined,
93
+ },
94
+ period: {
95
+ startDate: toOptionalDate(payload.period.start_date),
96
+ endDate: toOptionalDate(payload.period.end_date),
97
+ },
98
+ statementDate: toOptionalDate(payload.statement_date),
99
+ dataSource: toReconciliationReportDataSource(payload.data_source),
100
+ currency,
101
+ status: {
102
+ code: toReconciliationStatusCodeType(payload.status.code),
103
+ label: payload.status.label,
104
+ },
105
+ reconciledAt: toOptionalDate(payload.reconciled_at),
106
+ reconciledBy: {
107
+ userId: payload.reconciled_by.user_id ?? undefined,
108
+ name: payload.reconciled_by.name ?? undefined,
109
+ },
110
+ disclaimer: payload.disclaimer,
111
+ reportReference: payload.report_reference ?? undefined,
112
+ locked: payload.locked,
113
+ summary: {
114
+ openingBalance: toOptionalAmount(payload.summary.opening_balance, currency),
115
+ clearedPayments: mapCountAmount(payload.summary.cleared_payments, currency),
116
+ clearedDeposits: mapCountAmount(payload.summary.cleared_deposits, currency),
117
+ closingBalance: toOptionalAmount(payload.summary.closing_balance, currency),
118
+ unclearedAsOfStatementDate: {
119
+ payments: mapCountAmount(payload.summary.uncleared_as_of_statement_date.payments, currency),
120
+ deposits: mapCountAmount(payload.summary.uncleared_as_of_statement_date.deposits, currency),
121
+ },
122
+ ledgerClosingAsOfStatementDate: toOptionalAmount(payload.summary.ledger_closing_as_of_statement_date, currency),
123
+ clearedAfterStatementDate: payload.summary.cleared_after_statement_date != null
124
+ ? mapCountAmount(payload.summary.cleared_after_statement_date, currency)
125
+ : undefined,
126
+ unclearedAfterStatementDate: payload.summary.uncleared_after_statement_date != null
127
+ ? mapCountAmount(payload.summary.uncleared_after_statement_date, currency)
128
+ : undefined,
129
+ ledgerBalanceAsOfReconciledOn: toOptionalAmount(payload.summary.ledger_balance_as_of_reconciled_on, currency),
130
+ bridge: {
131
+ bankClosing: toOptionalAmount(payload.summary.bridge.bank_closing, currency),
132
+ unclearedDeposits: toAmountWC(payload.summary.bridge.uncleared_deposits, currency),
133
+ unclearedPayments: toAmountWC(payload.summary.bridge.uncleared_payments, currency),
134
+ computedLedgerClosing: toOptionalAmount(payload.summary.bridge.computed_ledger_closing, currency),
135
+ ledgerClosing: toOptionalAmount(payload.summary.bridge.ledger_closing, currency),
136
+ variance: toOptionalAmount(payload.summary.bridge.variance, currency),
137
+ },
138
+ variance: toOptionalAmount(payload.summary.variance, currency),
139
+ },
140
+ details: {
141
+ cleared: mapPaymentsDeposits(payload.details.cleared, currency),
142
+ carriedForward: mapPaymentsDeposits(payload.details.carried_forward, currency),
143
+ unclearedAsOfStatementDate: mapPaymentsDeposits(payload.details.uncleared_as_of_statement_date, currency),
144
+ },
145
+ };
146
+ }
@@ -0,0 +1,4 @@
1
+ import recordGet from 'lodash/get';
2
+ export function getReconciliationReportByReconciliationId(reconciliationReportState, reconciliationId) {
3
+ return recordGet(reconciliationReportState.reportsByReconciliationId, reconciliationId, undefined);
4
+ }
@@ -0,0 +1,10 @@
1
+ import { stringToUnion } from '../../commonStateTypes/stringToUnion';
2
+ const RECONCILIATION_REPORT_MATCH_TYPE = ['automatic', 'manual'];
3
+ export const toReconciliationReportMatchType = (value) => {
4
+ if (value === 'auto') {
5
+ return 'automatic';
6
+ }
7
+ return stringToUnion(value, RECONCILIATION_REPORT_MATCH_TYPE);
8
+ };
9
+ const RECONCILIATION_REPORT_DATA_SOURCE = ['Statement', 'Plaid'];
10
+ export const toReconciliationReportDataSource = (value) => stringToUnion(value, RECONCILIATION_REPORT_DATA_SOURCE);
package/lib/esm/epic.js CHANGED
@@ -442,6 +442,7 @@ import { fetchEntityRecommendationsForLineUpdateEpic, } from './view/recommendat
442
442
  import { fetchRecommendationByEntityIdEpic, } from './view/recommendation/fetchRecommendationByEntityIdEpic';
443
443
  import { fetchRecommendationByEntityNameEpic, } from './view/recommendation/fetchRecommendationByEntityNameEpic';
444
444
  import { submitRecommendationFeedbackEpic, } from './view/recommendation/submitRecommendationFeedbackEpic';
445
+ import { fetchReconciliationReportEpic, } from './view/reconciliationReportView/epic/fetchReconciliationReportEpic';
445
446
  import { fetchReferralsEpic, } from './view/referralView/epics/fetchReferralsEpic';
446
447
  import { fetchRewardsPlanEpic, } from './view/referralView/epics/fetchRewardsPlanEpic';
447
448
  import { resendReferralInviteEpic, } from './view/referralView/epics/resendReferralInviteEpic';
@@ -1171,6 +1172,7 @@ const ALL_EPICS = [
1171
1172
  fetchRecommendationForAccountTypeEpic,
1172
1173
  fetchRecommendationsAndUpdateMerchantRecommendationsEpic,
1173
1174
  fetchRecommendedTransactionRowIndexEpic,
1175
+ fetchReconciliationReportEpic,
1174
1176
  fetchReferralsEpic,
1175
1177
  fetchRegisteredInterestsEpic,
1176
1178
  fetchReimbursementCardEpic,
package/lib/esm/index.js CHANGED
@@ -196,7 +196,7 @@ import { clearStatementDateConflict, confirmStatementMapping, deleteAccountState
196
196
  import { backgroundRefetchReviewTab, clearExpenseAutomationTransactionsView, fetchTransactionCategorization, fetchTransactionCategorizationFailure, fetchTransactionCategorizationView, initializeTransactionCategorizationViewLocalData, markCategoryClassRecommendationsFailureForCategorization, markTransactionAsNotMiscategorized, removeTransactionFromAllTabs, saveTransactionCategorization, saveTransactionCategorizationLocalData, setAllItemsToCategoryClassInLocalDataForCategorization, setEntityRecommendationForLineIdsForCategorization, syncTransactionCategorizationFromDetailSave, updateCurrentSelectedTransactionCategorizationTab, updateSelectedCheckboxTransactionIds, updateSelectedCustomerForTransaction, updateSelectedTransactionId, updateSelectedVendorForTransaction, updateTransactionCategorization, updateTransactionCategorizationCompletedSubTab, updateTransactionCategorizationSaveStatus, updateTransactionCategorizationUIState, updateTransactionCategorizationUploadReceiptState, updateTransactionFilters, updateTransactionGroup, uploadTransactionCategorizationReceiptSuccess, } from './view/expenseAutomationView/reducers/transactionsViewReducer';
197
197
  import { getExpenseAutomationFluxAnalysisView } from './view/expenseAutomationView/selectors/fluxAnalysisViewSelector';
198
198
  import { getNewAccrualAccountsView as getJeNewAccrualAccountsView, getNewScheduleSearchTransactionsView as getJeNewScheduleSearchTransactionsView, getNewScheduleView as getJeNewScheduleView, } from './view/expenseAutomationView/selectors/jeNewScheduleViewSelector';
199
- import { getExpenseAutomationReconciliationView, getLedgerAccountIdsWithStatementOverrideWarning, getLedgerAccountsForCombinedStatementMapping, getReparseStatementStatusByAccountId, getUnavailableLedgerAccountIds, isAccountReconReport, } from './view/expenseAutomationView/selectors/reconciliationViewSelector';
199
+ import { getAccountReconciliationRecordsBySelectedPeriod, getExpenseAutomationReconciliationView, getLedgerAccountIdsWithStatementOverrideWarning, getLedgerAccountsForCombinedStatementMapping, getReparseStatementStatusByAccountId, getUnavailableLedgerAccountIds, isAccountReconReport, } from './view/expenseAutomationView/selectors/reconciliationViewSelector';
200
200
  import { getExpenseAutomationTransactionView, getLastTransferEntryReplacement, } from './view/expenseAutomationView/selectors/transactionCategorizationSelector';
201
201
  import { applyTransactionFilters } from './view/expenseAutomationView/transactionFilterHelpers';
202
202
  import { TRANSACTION_FILTER_CATEGORIES, } from './view/expenseAutomationView/transactionFilterTypes';
@@ -274,6 +274,8 @@ import { fetchProjectList } from './view/projectList/projectListReducer';
274
274
  import { getProjectList, } from './view/projectList/projectListSelector';
275
275
  import { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityId, fetchRecommendationByEntityName, submitRecommendationFeedback, } from './view/recommendation/recommendationReducer';
276
276
  import { getRecommendationFeedbackSubmitState } from './view/recommendation/recommendationSelector';
277
+ import { fetchReconciliationReport } from './view/reconciliationReportView/reconciliationReportViewReducer';
278
+ import { getReconciliationReport } from './view/reconciliationReportView/reconciliationReportViewSelector';
277
279
  import { DEFAULT_REFERRER_AMOUNT, clearReferrals, fetchReferrals, fetchRewardsPlan, resendReferralInvite, saveReferralFormDataInLocalStore, sendReferralInvite, updateReferViewed, updateReferralListSortUiState, } from './view/referralView/referralReducer';
278
280
  import { getInviteFormView, getReferralListView, getRewardsPlanCard, } from './view/referralView/referralSelector';
279
281
  import { AmountStatusTypes, StatusTypes, toReferralListViewSortKeyType, } from './view/referralView/referralState';
@@ -495,7 +497,7 @@ export {
495
497
  // Bulk Upload Types
496
498
  BATCH_FILE_STATUSES, isUnmatchedTabFileStatus, DEFAULT_COMPLETED_SUB_TAB, DEFAULT_TRANSACTION_GROUP, TRANSACTION_GROUPS, toTransactionGroup, toBatchFileStatus, toBatchStatusValue, toBulkUploadPhase, toBulkUploadResultsTab, toBulkUploadSortKey, toCompletedSubTab, toMatchSource, toMissingReceiptsTab, ALL_ACTUAL_MATCHING_IGNORE_REASONS, actualMatchingTransactionLineKey, ALL_JE_PAGE_TABS, MATCH_ACCRUAL_SEARCH_LIMIT, MATCH_SEARCH_DEBOUNCE_MS, MATCH_TRANSACTION_SEARCH_PAGE_SIZE, toExpenseAutomationJEScheduleSortKey, toExpenseAutomationActualMatchingSortKey, toExpenseAutomationActualMatchingStatusFilter, toActualMatchingIgnoreReasonType, toJEPageTab, toMatchAndReverseResultStatus, toUndoReversalResultStatus, toggleJECreatedGroupCollapsed, toggleJEFailedGroupCollapsed, toggleJEPendingGroupCollapsed, updateJEScheduleTypeFilter, getExpenseAutomationView, toExpenseAutomationMissingReceiptsSortKey, toExpenseAutomationTransactionsTabKey, toExpenseAutomationViewType, toExpenseAutomationViewTypeStrict, uploadExpenseAutomationMissingReceiptSuccess, markExpenseAutomationMissingReceiptAsDone, fetchAllExpenseAutomationTabs, refreshExpenseAutomationCurrentTab, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationCompletedSubTab, updateTransactionGroup, fetchExpenseAutomationMissingReceipts,
497
499
  // Bulk Upload Actions
498
- bulkUploadReceipts, bulkUploadAutomatchingTimedOut, bulkUploadReceiptsSuccess, bulkUploadReceiptsFailure, restoreBulkUploadAutomatchingOnMount, restoreBulkUploadMatchingState, updateBulkUploadProgress, pusherBatchStatusUpdate, requestMissingReceiptsTabNavigation, clearMissingReceiptsTabNavigation, fetchBulkUploadBatchDetails, fetchBulkUploadBatchDetailsSuccess, fetchBulkUploadBatchDetailsFailure, storeBatchDetails, fetchMoreBatchDetails, fetchMoreBatchDetailsComplete, fetchMoreBatchDetailsFailure, fetchBulkUploadBatches, fetchBulkUploadBatchesSuccess, fetchBulkUploadBatchesFailure, clearBulkUploadBatchDetailsForScopeChange, markBatchDetailRefreshAttempted, refreshBatchDetailsForBatchId, confirmBulkUploadMatch, confirmBulkUploadMatchSuccess, confirmBulkUploadMatchFailure, setBulkUploadResultsTab, setBulkUploadCompletedSubTab, setBulkUploadSortConfig, setBulkUploadUploadedFileCount, clearBulkUpload, searchTransactionsForManualMatch, searchTransactionsForManualMatchSuccess, searchTransactionsForManualMatchFailure, clearManualSearchResults, acknowledgeBulkUploadConfirmMatchComplete, fetchCompletedTransactions, fetchCompletedTransactionsSuccess, fetchCompletedTransactionsFailure, fetchFluxAnalysisView, clearExpenseAutomationFluxAnalysisView, updateOperatingExpensesIdsForReview as updateFluxOperatingExpensesIdsForReview, updateSelectedSectionIdsForReview as updateFluxAnalysisSelectedSectionIdsForReview, reviewFluxAnalysisView, updateExpenseAutomationMissingReceiptUploadState, updateExpenseAutomationMissingReceiptsUIState, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, getExpenseAutomationFluxAnalysisView, updateCurrentSelectedView, updateCurrentSelectedPeriod, getExpenseAutomationTransactionView, getLastTransferEntryReplacement, updateFluxAnalysisViewUIState, updateFluxAnalysisViewPageMetaData, MAX_SELECTION_LIMIT, checkIfAllLineItemsAreCategoryClassFilled, getLineItemsByTransactionIdsFromLocalData, isAnyItemWithUncategorizedExpenseAccount, excludeAccountFromReconciliation, includeAccountInReconciliation, saveExpenseAutomationReconciliationDetail, updateExpenseAutomationReconcileTabListScrollState, updateExpenseAutomationReconReviewTabListSortState, updateExpenseAutomationReconcileTabListSortState, updateExpenseAutomationReconcileTabLocalData, updateExpenseAutomationAccountReconciliationSelectedTab, updateExpenseAutomationAccountReconciliationSelectedAccountId, getExpenseAutomationReconciliationView, getLedgerAccountIdsWithStatementOverrideWarning, getLedgerAccountsForCombinedStatementMapping, getReparseStatementStatusByAccountId, getUnavailableLedgerAccountIds, fetchReconciliationView, uploadAccountStatementIntoDocumentAI, updateExpenseAutomationReconListScrollPosition, setConnectionInProgressForAccountReconciliation, getAccountReconByAccountIdAndSelectedPeriod, toReconciliationTabsType, isAccountReconReport, updateExpenseAutomationReconReviewTabLocalData, updateExpenseAutomationSelectedDrawerAccountId, updateStatementProcessingFailed, saveExpenseAutomationReconciliationReview, updateExpenseAutomationAccountReconciliationLocalData, toReconciliationAccountSource, deleteAccountStatement, deleteCombinedStatement, uploadAccountStatement, parseCombinedStatement, parseStatement, parseStatementSuccess, parseStatementFailure, reparseStatement, reparseStatementSuccess, reparseStatementFailure, resetReparseStatementStatus, updateParsedStatementData, updateNodeCollapseState, updateStatementUploadChosen, submitCombinedStatementUpdate, submitStatementUpdate, clearStatementDateConflict, confirmStatementMapping, updateStatementUpdateLocalData, isReviewTransactionBankTransferType, isReviewTransactionBillPaymentType, isReviewTransactionCreditCardPaymentType, isReviewTransactionDepositType, isReviewTransactionExpenseType, isReviewTransactionCreditCardCreditType, setStatementParseInProgress, setStatementProcessingDismissedToBackground, updateStatementProcessingBackgroundCompletedSteps, };
500
+ bulkUploadReceipts, bulkUploadAutomatchingTimedOut, bulkUploadReceiptsSuccess, bulkUploadReceiptsFailure, restoreBulkUploadAutomatchingOnMount, restoreBulkUploadMatchingState, updateBulkUploadProgress, pusherBatchStatusUpdate, requestMissingReceiptsTabNavigation, clearMissingReceiptsTabNavigation, fetchBulkUploadBatchDetails, fetchBulkUploadBatchDetailsSuccess, fetchBulkUploadBatchDetailsFailure, storeBatchDetails, fetchMoreBatchDetails, fetchMoreBatchDetailsComplete, fetchMoreBatchDetailsFailure, fetchBulkUploadBatches, fetchBulkUploadBatchesSuccess, fetchBulkUploadBatchesFailure, clearBulkUploadBatchDetailsForScopeChange, markBatchDetailRefreshAttempted, refreshBatchDetailsForBatchId, confirmBulkUploadMatch, confirmBulkUploadMatchSuccess, confirmBulkUploadMatchFailure, setBulkUploadResultsTab, setBulkUploadCompletedSubTab, setBulkUploadSortConfig, setBulkUploadUploadedFileCount, clearBulkUpload, searchTransactionsForManualMatch, searchTransactionsForManualMatchSuccess, searchTransactionsForManualMatchFailure, clearManualSearchResults, acknowledgeBulkUploadConfirmMatchComplete, fetchCompletedTransactions, fetchCompletedTransactionsSuccess, fetchCompletedTransactionsFailure, fetchFluxAnalysisView, clearExpenseAutomationFluxAnalysisView, updateOperatingExpensesIdsForReview as updateFluxOperatingExpensesIdsForReview, updateSelectedSectionIdsForReview as updateFluxAnalysisSelectedSectionIdsForReview, reviewFluxAnalysisView, updateExpenseAutomationMissingReceiptUploadState, updateExpenseAutomationMissingReceiptsUIState, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, getExpenseAutomationFluxAnalysisView, updateCurrentSelectedView, updateCurrentSelectedPeriod, getExpenseAutomationTransactionView, getLastTransferEntryReplacement, updateFluxAnalysisViewUIState, updateFluxAnalysisViewPageMetaData, MAX_SELECTION_LIMIT, checkIfAllLineItemsAreCategoryClassFilled, getLineItemsByTransactionIdsFromLocalData, isAnyItemWithUncategorizedExpenseAccount, excludeAccountFromReconciliation, includeAccountInReconciliation, saveExpenseAutomationReconciliationDetail, updateExpenseAutomationReconcileTabListScrollState, updateExpenseAutomationReconReviewTabListSortState, updateExpenseAutomationReconcileTabListSortState, updateExpenseAutomationReconcileTabLocalData, updateExpenseAutomationAccountReconciliationSelectedTab, updateExpenseAutomationAccountReconciliationSelectedAccountId, getAccountReconciliationRecordsBySelectedPeriod, getExpenseAutomationReconciliationView, getLedgerAccountIdsWithStatementOverrideWarning, getLedgerAccountsForCombinedStatementMapping, getReparseStatementStatusByAccountId, getUnavailableLedgerAccountIds, fetchReconciliationView, uploadAccountStatementIntoDocumentAI, updateExpenseAutomationReconListScrollPosition, setConnectionInProgressForAccountReconciliation, getAccountReconByAccountIdAndSelectedPeriod, toReconciliationTabsType, isAccountReconReport, updateExpenseAutomationReconReviewTabLocalData, updateExpenseAutomationSelectedDrawerAccountId, updateStatementProcessingFailed, saveExpenseAutomationReconciliationReview, updateExpenseAutomationAccountReconciliationLocalData, toReconciliationAccountSource, deleteAccountStatement, deleteCombinedStatement, uploadAccountStatement, parseCombinedStatement, parseStatement, parseStatementSuccess, parseStatementFailure, reparseStatement, reparseStatementSuccess, reparseStatementFailure, resetReparseStatementStatus, updateParsedStatementData, updateNodeCollapseState, updateStatementUploadChosen, submitCombinedStatementUpdate, submitStatementUpdate, clearStatementDateConflict, confirmStatementMapping, updateStatementUpdateLocalData, isReviewTransactionBankTransferType, isReviewTransactionBillPaymentType, isReviewTransactionCreditCardPaymentType, isReviewTransactionDepositType, isReviewTransactionExpenseType, isReviewTransactionCreditCardCreditType, setStatementParseInProgress, setStatementProcessingDismissedToBackground, updateStatementProcessingBackgroundCompletedSteps, };
499
501
  export { computeNewScheduleJeDetails, createOneTimeAccrual, createOneTimeAccrualFailure, createOneTimeAccrualSuccess, fetchOneTimeAccrual, getJeNewAccrualAccountsView, ignoreOneTimeAccrual, promoteOneTimeAccrual, revertJeSchedule, revertOneTimeAccrual, getJeNewScheduleSearchTransactionsView, getJeNewScheduleView, };
500
502
  export { fetchTransactionCategorization, fetchTransactionCategorizationView, updateTransactionCategorizationUIState, updateTransactionFilters, updateSelectedCheckboxTransactionIds, markCategoryClassRecommendationsFailureForCategorization, setEntityRecommendationForLineIdsForCategorization, initializeTransactionCategorizationViewLocalData, setAllItemsToCategoryClassInLocalDataForCategorization, saveTransactionCategorizationLocalData, fetchTransactionCategorizationFailure, saveTransactionCategorization, updateTransactionCategorization, updateTransactionCategorizationSaveStatus, markTransactionAsNotMiscategorized, updateSelectedVendorForTransaction, updateSelectedCustomerForTransaction, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, createTransferEntry, createTransferEntryFailure, createTransferEntryReplacedTransaction, createTransferEntrySuccess, resetCreateTransferEntryStatus, clearTransferEntryRouteReplacement, fetchAccountsForTransferFlow, removeTransactionFromAllTabs, clearExpenseAutomationTransactionsView, toTransactionsSortKey, };
501
503
  export { TOP_EX_TIME_PERIODS };
@@ -578,6 +580,7 @@ export { getEntityAutoCompleteResults };
578
580
  export { updateEntities };
579
581
  export { getEntityByEntityIDs };
580
582
  export { getAddressByAddressId };
583
+ export { fetchReconciliationReport, getReconciliationReport, };
581
584
  export { ALL_ACCOUNTING_CONNECTION_CREATION_MODES, isCompanyEligibleForTreasury, isCardsProductEnabledOnCompany, filterChargeCardRolesWhenProductDisabled, };
582
585
  export { getCompanyByCompanyId, getControllersForCompany, getCompanyInfoForAllCockpitCompaniesInState, getFirstControllerEmail, getFirstControllerId, getControllersName, };
583
586
  export { getCompanyManagementView, getPlanListZeniSku, getAddonListZeniSku, };
@@ -38,10 +38,10 @@ import invoicingCoupon, { initialState as initialInvoicingCouponState, } from '.
38
38
  import invoicingCreditNote, { initialState as initialInvoicingCreditNoteState, } from './entity/invoicing/creditNote/creditNoteReducer';
39
39
  import invoicingDunningCase, { initialState as initialInvoicingDunningCaseState, } from './entity/invoicing/dunningCase/dunningCaseReducer';
40
40
  import invoicingInvoice, { initialState as initialInvoicingInvoiceState, } from './entity/invoicing/invoice/invoiceReducer';
41
+ import invoicingCatalog, { initialState as initialInvoicingCatalogState, } from './entity/invoicing/invoicingCatalog/invoicingCatalogReducer';
41
42
  import invoicingCustomer, { initialState as initialInvoicingCustomerState, } from './entity/invoicing/invoicingCustomer/invoicingCustomerReducer';
42
43
  import invoicingSubscription, { initialState as initialInvoicingSubscriptionState, } from './entity/invoicing/invoicingSubscription/invoicingSubscriptionReducer';
43
44
  import invoicingTransaction, { initialState as initialInvoicingTransactionState, } from './entity/invoicing/invoicingTransaction/invoicingTransactionReducer';
44
- import invoicingCatalog, { initialState as initialInvoicingCatalogState, } from './entity/invoicing/invoicingCatalog/invoicingCatalogReducer';
45
45
  import jeSchedules, { initialState as initialJESchedulesState, } from './entity/jeSchedules/jeSchedulesReducer';
46
46
  import merchant, { initialState as initialMerchantState, } from './entity/merchant/merchantReducer';
47
47
  import monthEndCloseChecks, { initialState as initialMonthEndCloseChecksState, } from './entity/monthEndCloseChecks/monthEndCloseChecksReducer';
@@ -52,6 +52,7 @@ import paymentAccount, { initialState as initialPaymentAccountState, } from './e
52
52
  import paymentInstrument, { initialState as initialPaymentInstrumentState, } from './entity/paymentInstrument/paymentInstrumentReducer';
53
53
  import accountingSummary, { initialState as initialAccountingSummaryState, } from './entity/portfolio/accountingSummary/accountingSummaryReducer';
54
54
  import project, { initialState as initialProjectState, } from './entity/project/projectReducer';
55
+ import reconciliationReport, { initialState as initialReconciliationReportState, } from './entity/reconciliationReport/reconciliationReportReducer';
55
56
  import reimbursement, { initialState as initialReimbursementState, } from './entity/reimbursement/reimbursementReducer';
56
57
  import sectionAccountsView, { initialState as initialSectionAccountsViewState, } from './entity/sectionAccountsView/sectionAccountsViewReducer';
57
58
  import sectionClassesViewV2, { initialState as initialSectionClassesViewStateV2, } from './entity/sectionClassesViewV2/sectionClassesViewReducer';
@@ -179,6 +180,7 @@ import profitAndLossClassesView, { initialState as initialProfitAndLossClassesVi
179
180
  import profitAndLossProjectView, { initialState as initialProfitAndLossProjectViewState, } from './view/profitAndLossProjectView/profitAndLossProjectViewReducer';
180
181
  import projectList, { initialState as initialProjectListState, } from './view/projectList/projectListReducer';
181
182
  import recommendation, { initialState as initialEntityRecommendationState, } from './view/recommendation/recommendationReducer';
183
+ import reconciliationReportView, { initialState as initialReconciliationReportViewState, } from './view/reconciliationReportView/reconciliationReportViewReducer';
182
184
  import referralView, { initialState as initialReferralViewState, } from './view/referralView/referralReducer';
183
185
  import reimbursementCard, { initialState as initialReimbursementCardState, } from './view/reimbursementCard/reimbursementCardReducer';
184
186
  import reportUIOptionsReducer, { initialState as initialReportUIOptionsState, } from './view/reportUIOptions/reportUIOptionsReducer';
@@ -325,6 +327,7 @@ const initialEntitiesState = {
325
327
  onboardingCustomerState: initialOnboardingCustomerState,
326
328
  paymentAccountState: initialPaymentAccountState,
327
329
  paymentInstrumentState: initialPaymentInstrumentState,
330
+ reconciliationReportState: initialReconciliationReportState,
328
331
  recurringBillState: initialRecurringBillState,
329
332
  reimbursementConfigState: initialReimbursementConfigState,
330
333
  reimbursementState: initialReimbursementState,
@@ -493,6 +496,7 @@ const initialViewsState = {
493
496
  profitAndLossState: initialProfitAndLossState,
494
497
  projectListState: initialProjectListState,
495
498
  realTimeApprovalState: initialRealTimeApprovalState,
499
+ reconciliationReportViewState: initialReconciliationReportViewState,
496
500
  referralViewState: initialReferralViewState,
497
501
  reimbursementCardState: initialReimbursementCardState,
498
502
  reimbursementConfigState: initialReimbursementConfigState,
@@ -611,6 +615,7 @@ const entityReducers = {
611
615
  onboardingCustomerState: onboardingCustomer,
612
616
  paymentAccountState: paymentAccount,
613
617
  paymentInstrumentState: paymentInstrument,
618
+ reconciliationReportState: reconciliationReport,
614
619
  recurringBillState: recurringBill,
615
620
  reimbursementConfigState: reimbursementConfig,
616
621
  reimbursementState: reimbursement,
@@ -779,6 +784,7 @@ const viewReducers = {
779
784
  profitAndLossState: profitAndLoss,
780
785
  projectListState: projectList,
781
786
  realTimeApprovalState: realTimeApproval,
787
+ reconciliationReportViewState: reconciliationReportView,
782
788
  referralViewState: referralView,
783
789
  reimbursementCardState: reimbursementCard,
784
790
  reimbursementConfigState: reimbursementConfig,
@@ -4,7 +4,7 @@ import keys from 'lodash/keys';
4
4
  import omit from 'lodash/omit';
5
5
  import orderBy from 'lodash/orderBy';
6
6
  import { reduceAnyFetchState } from '../../../commonStateTypes/reduceFetchState';
7
- import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
7
+ import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
8
8
  import { getAccountsByType } from '../../../entity/account/accountSelector';
9
9
  import { getAccountReconByAccountIdAndSelectedPeriod } from '../../../entity/accountRecon/accountReconSelector';
10
10
  import { getCurrentTenant } from '../../../entity/tenant/tenantSelector';
@@ -473,6 +473,10 @@ export const getExpenseAutomationReconciliationView = createSelector((state) =>
473
473
  summary: summaryWithTimeSaved,
474
474
  };
475
475
  });
476
+ export function getAccountReconciliationRecordsBySelectedPeriod(state) {
477
+ return state.expenseAutomationReconciliationViewState
478
+ .accountReconciliationRecordsBySelectedPeriod;
479
+ }
476
480
  export const getReparseStatementStatusByAccountId = (state, accountId) => getExpenseAutomationReconciliationView(state)
477
481
  .accountReconciliationsByAccountID[accountId]?.reparseStatementStatus;
478
482
  export function getLedgerAccountsForCombinedStatementMapping(accountReconciliationsByAccountID, selectedAccountId) {
@@ -2,7 +2,7 @@ import { from } from 'rxjs';
2
2
  import { filter, mergeMap } from 'rxjs/operators';
3
3
  import { fetchInvoicingCatalogPlanList, fetchInvoicingCatalogProductList, } from '../../invoicingCatalogListView/invoicingCatalogListViewReducer';
4
4
  import { fetchInvoicingCustomerList } from '../../invoicingCustomerListView/invoicingCustomerListViewReducer';
5
- import { INVOICING_LIST_ALL_TAB, getInvoicingListTab, } from '../../invoicingListTabCache';
5
+ import { INVOICING_LIST_ALL_TAB, getInvoicingListTab, shouldFetchFullInvoicingList, } from '../../invoicingListTabCache';
6
6
  import { fetchInvoicingSettings } from '../../settingsView/settingsViewReducer';
7
7
  import { fetchCreateInvoiceFormPage } from '../createInvoiceReducer';
8
8
  /**
@@ -13,6 +13,8 @@ import { fetchCreateInvoiceFormPage } from '../createInvoiceReducer';
13
13
  const shouldFetch = (fetchStateAndError, cacheOverride) => cacheOverride ||
14
14
  (fetchStateAndError.fetchState !== 'Completed' &&
15
15
  fetchStateAndError.fetchState !== 'In-Progress');
16
+ const PICKER_PLAN_FILTERS = { type: 'plan' };
17
+ const PICKER_PRODUCT_FILTERS = { type: 'addon|charge' };
16
18
  /**
17
19
  * Orchestrates the create-invoice form's option sources: on
18
20
  * `fetchCreateInvoiceFormPage`, fans out to the customer, catalog plan, catalog
@@ -34,15 +36,27 @@ export const fetchCreateInvoiceFormPageEpic = (actions$, state$) => actions$.pip
34
36
  tabId: INVOICING_LIST_ALL_TAB,
35
37
  }));
36
38
  }
37
- if (shouldFetch({ fetchState: catalogState.fetchState, error: catalogState.error }, cacheOverride)) {
39
+ if (shouldFetchFullInvoicingList({
40
+ fetchState: catalogState.fetchState,
41
+ filters: catalogState.planFilters,
42
+ nextCursor: catalogState.planNextCursor,
43
+ sort: catalogState.planSort,
44
+ }, PICKER_PLAN_FILTERS, cacheOverride)) {
38
45
  pageActions.push(fetchInvoicingCatalogPlanList({
39
- filters: { type: 'plan' },
46
+ fetchAll: true,
47
+ filters: PICKER_PLAN_FILTERS,
40
48
  keepExistingListItems: false,
41
49
  }));
42
50
  }
43
- if (shouldFetch(catalogState.productFetchState, cacheOverride)) {
51
+ if (shouldFetchFullInvoicingList({
52
+ fetchState: catalogState.productFetchState.fetchState,
53
+ filters: catalogState.productFilters,
54
+ nextCursor: catalogState.productNextCursor,
55
+ sort: catalogState.productSort,
56
+ }, PICKER_PRODUCT_FILTERS, cacheOverride)) {
44
57
  pageActions.push(fetchInvoicingCatalogProductList({
45
- filters: { type: 'addon|charge' },
58
+ fetchAll: true,
59
+ filters: PICKER_PRODUCT_FILTERS,
46
60
  keepExistingListItems: false,
47
61
  }));
48
62
  }
@@ -17,22 +17,25 @@ import { invoicingSubscriptionTodayIso, toInvoicingSubscriptionBillingCycles, to
17
17
  * Sending them made every price point whose cadence the form could not express
18
18
  * — a daily plan, for instance — fail with a 422.
19
19
  */
20
- export const localDataToSaveInvoicingSubscriptionPayload = (localData, shippingAddress) => {
20
+ export const localDataToSaveInvoicingSubscriptionPayload = (localData, shippingAddress, flatFeePriceCodes = new Set()) => {
21
+ const itemQuantity = (priceCode, quantity) => flatFeePriceCodes.has(priceCode)
22
+ ? 1
23
+ : toInvoicingSubscriptionQuantity(quantity);
21
24
  const items = [
22
25
  {
23
26
  item_price_code: localData.planPriceCode,
24
27
  item_type: 'plan',
25
- quantity: toInvoicingSubscriptionQuantity(localData.planQuantity),
28
+ quantity: itemQuantity(localData.planPriceCode, localData.planQuantity),
26
29
  },
27
30
  ...localData.addonPriceCodes.map((priceCode) => ({
28
31
  item_price_code: priceCode,
29
32
  item_type: 'addon',
30
- quantity: toInvoicingSubscriptionQuantity(localData.addonQuantities[priceCode]),
33
+ quantity: itemQuantity(priceCode, localData.addonQuantities[priceCode]),
31
34
  })),
32
35
  ...localData.chargePriceCodes.map((priceCode) => ({
33
36
  item_price_code: priceCode,
34
37
  item_type: 'charge',
35
- quantity: toInvoicingSubscriptionQuantity(localData.chargeQuantities[priceCode]),
38
+ quantity: itemQuantity(priceCode, localData.chargeQuantities[priceCode]),
36
39
  })),
37
40
  ];
38
41
  const setupFee = localData.setupFee?.amount;
@@ -59,11 +62,59 @@ export const localDataToSaveInvoicingSubscriptionPayload = (localData, shippingA
59
62
  };
60
63
  };
61
64
  const toEstimateDate = (value) => value != null && value !== '' ? date(value) : undefined;
65
+ const isItemLevelCoupon = (discount) => discount.entity_type === 'item_level_coupon';
66
+ const isCoupon = (discount) => discount.entity_type === 'document_level_coupon' ||
67
+ discount.entity_type === 'item_level_coupon';
68
+ /**
69
+ * Name the coupon behind a line's discount. Exact when the API links a coupon
70
+ * row to its line (`line_item_id`); otherwise only when a single item-level
71
+ * coupon applies, since then every discounted line got that one.
72
+ */
73
+ const lineDiscountLabel = (lineItem, discounts) => {
74
+ if (lineItem.discount_amount <= 0) {
75
+ return undefined;
76
+ }
77
+ const itemCoupons = discounts.filter(isItemLevelCoupon);
78
+ const linked = lineItem.id != null
79
+ ? itemCoupons.filter((coupon) => coupon.line_item_id === lineItem.id)
80
+ : [];
81
+ if (linked.length > 0) {
82
+ return [...new Set(linked.map((coupon) => coupon.description))].join(', ');
83
+ }
84
+ const names = [...new Set(itemCoupons.map((coupon) => coupon.description))];
85
+ return names.length === 1 ? names[0] : undefined;
86
+ };
87
+ /**
88
+ * One row per coupon. An item-level coupon comes back once per line it
89
+ * discounted, so its rows are summed; listing each would read as the same
90
+ * discount taken several times.
91
+ */
92
+ const mergeCouponRows = (discounts) => {
93
+ const rowsByCoupon = new Map();
94
+ discounts.filter(isCoupon).forEach((coupon) => {
95
+ // The API sends `entity_id: ""` when it has none; key those by name so
96
+ // two unrelated coupons are not merged into one row.
97
+ const couponKey = coupon.entity_id != null && coupon.entity_id !== ''
98
+ ? coupon.entity_id
99
+ : coupon.description;
100
+ const key = `${coupon.entity_type}:${couponKey}`;
101
+ const row = rowsByCoupon.get(key);
102
+ rowsByCoupon.set(key, {
103
+ amount: Math.round(((row?.amount ?? 0) + coupon.amount) * 100) / 100,
104
+ description: coupon.description,
105
+ });
106
+ });
107
+ return [...rowsByCoupon.values()];
108
+ };
62
109
  const mapInvoiceEstimate = (payload, money) => payload == null
63
110
  ? undefined
64
111
  : {
65
112
  amountDue: money(payload.amount_due),
66
113
  couponDiscountTotal: money(payload.coupon_discount_total),
114
+ coupons: mergeCouponRows(payload.discounts ?? []).map((coupon) => ({
115
+ amount: money(coupon.amount),
116
+ description: coupon.description,
117
+ })),
67
118
  creditsApplied: money(payload.credits_applied),
68
119
  dueDate: toEstimateDate(payload.due_date),
69
120
  grossSubTotal: money(payload.gross_sub_total),
@@ -74,6 +125,8 @@ const mapInvoiceEstimate = (payload, money) => payload == null
74
125
  dateTo: toEstimateDate(lineItem.date_to),
75
126
  description: lineItem.description,
76
127
  discountAmount: money(lineItem.discount_amount),
128
+ discountLabel: lineDiscountLabel(lineItem, payload.discounts ?? []),
129
+ grossAmount: money(lineItem.gross_amount ?? lineItem.amount),
77
130
  quantity: lineItem.quantity,
78
131
  taxAmount: lineItem.tax_amount != null
79
132
  ? money(lineItem.tax_amount)
@@ -7,7 +7,7 @@ import { getInvoicingCatalogListView, getInvoicingCatalogPrices, } from '../invo
7
7
  import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
8
8
  import { getInvoicingAllCustomers } from '../invoicingCustomerListView/invoicingCustomerListViewSelector';
9
9
  import { getInvoicingFormDraftLocalData, isInvoicingFormDraftPending, } from '../invoicingFormDraft';
10
- import { initialInvoicingSubscriptionFormLocalData } from './invoicingSubscriptionFormConfig';
10
+ import { initialInvoicingSubscriptionFormLocalData, invoicingPriceHasQuantity, } from './invoicingSubscriptionFormConfig';
11
11
  import { INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE } from './invoicingSubscriptionFormConfig';
12
12
  const notStartedEstimateView = {
13
13
  creditsMayChange: false,
@@ -175,6 +175,7 @@ const toInvoicingPlanOption = (plan) => ({
175
175
  // price point and rejects a subscription that claims a different one.
176
176
  billingPeriod: plan.period,
177
177
  billingPeriodUnit: plan.periodUnit,
178
+ hasQuantity: invoicingPriceHasQuantity(plan.pricingModel),
178
179
  label: plan.name ?? plan.id,
179
180
  price: invoicingUnitPrice(plan),
180
181
  productCode: plan.productCode,
@@ -186,6 +187,7 @@ const toInvoicingCatalogItemOption = (product) => ({
186
187
  // A one-time charge has no cadence, so its price is quoted flat rather than
187
188
  // per period.
188
189
  billingPeriodUnit: product.itemType === 'charge' ? undefined : product.periodUnit,
190
+ hasQuantity: invoicingPriceHasQuantity(product.pricingModel),
189
191
  label: product.name ?? product.id,
190
192
  price: invoicingUnitPrice(product),
191
193
  productCode: product.productCode,
@@ -4,7 +4,7 @@ import { fetchInvoicingCouponList } from '../couponView/couponViewReducer';
4
4
  import { getInvoicingCouponListFetchState } from '../couponView/couponViewSelector';
5
5
  import { fetchInvoicingCatalogPlanList } from '../invoicingCatalogListView/invoicingCatalogListViewReducer';
6
6
  import { fetchInvoicingConfig } from '../invoicingConfigView/invoicingConfigViewReducer';
7
- import { INVOICING_LIST_ALL_TAB } from '../invoicingListTabCache';
7
+ import { INVOICING_LIST_ALL_TAB, shouldFetchFullInvoicingList, } from '../invoicingListTabCache';
8
8
  import { fetchInvoicingSubscriptionDetail, fetchInvoicingSubscriptionDetailPage, } from './editInvoicingSubscriptionDetailViewReducer';
9
9
  /**
10
10
  * Fetch unless the endpoint is already loaded or in flight — or always when the
@@ -40,11 +40,14 @@ export const fetchInvoicingSubscriptionDetailPageEpic = (actions$, state$) => ac
40
40
  // Plan and coupon lists back the names the page renders for its line
41
41
  // items; both list slices cache, so this only refetches when asked to.
42
42
  const catalogListState = state.invoicingCatalogListViewState;
43
- if (shouldFetch({
44
- error: catalogListState.error,
43
+ if (shouldFetchFullInvoicingList({
45
44
  fetchState: catalogListState.fetchState,
46
- }, cacheOverride)) {
45
+ filters: catalogListState.planFilters,
46
+ nextCursor: catalogListState.planNextCursor,
47
+ sort: catalogListState.planSort,
48
+ }, { type: 'plan' }, cacheOverride)) {
47
49
  pageActions.push(fetchInvoicingCatalogPlanList({
50
+ fetchAll: true,
48
51
  filters: { type: 'plan' },
49
52
  keepExistingListItems: false,
50
53
  }));
@@ -20,6 +20,7 @@ export const fetchInvoicingSubscriptionFormPageEpic = (actions$) => actions$.pip
20
20
  // unfiltered list would replace the store's plans with every catalog
21
21
  // item.
22
22
  fetchInvoicingCatalogPlanList({
23
+ fetchAll: true,
23
24
  filters: { type: 'plan' },
24
25
  keepExistingListItems: false,
25
26
  }),
@@ -22,6 +22,8 @@ const subscriptionItemQuantities = (subscription, itemType) => {
22
22
  * their day gets their own calendar date rather than yesterday's.
23
23
  */
24
24
  export const invoicingSubscriptionTodayIso = () => dateNow().format('YYYY-MM-DD');
25
+ /** A flat fee bills its price once, so its quantity is always 1. */
26
+ export const invoicingPriceHasQuantity = (pricingModel) => pricingModel !== 'flat_fee';
25
27
  /**
26
28
  * Clamp a subscription-item quantity to the minimum the API accepts. Shared by
27
29
  * the plan, add-on and charge items so none of them can bill a 0 or a fraction.
@@ -9,10 +9,11 @@ import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload
9
9
  import { resetNewAddressDataInLocalStore } from '../../addressView/addressViewReducer';
10
10
  import { invalidateInvoicingCustomerDetail } from '../editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer';
11
11
  import { invalidateInvoiceListForCustomer } from '../invoiceList/invoiceListReducer';
12
+ import { getInvoicingCatalogPrices } from '../invoicingCatalogListView/invoicingCatalogListViewSelector';
12
13
  import { addInvoicingSubscriptionToList } from '../invoicingSubscriptionListView/invoicingSubscriptionListViewReducer';
13
14
  import { localDataToSaveInvoicingSubscriptionPayload, } from './editInvoicingSubscriptionDetailViewPayload';
14
15
  import { submitInvoicingSubscriptionForm, submitInvoicingSubscriptionFormFailure, submitInvoicingSubscriptionFormSuccess, syncInvoicingSubscriptionCoupons, } from './editInvoicingSubscriptionDetailViewReducer';
15
- import { INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE } from './invoicingSubscriptionFormConfig';
16
+ import { INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE, invoicingPriceHasQuantity, } from './invoicingSubscriptionFormConfig';
16
17
  const normalizeCouponCode = (value) => (value ?? '').trim().toUpperCase();
17
18
  export const isInvoicingSubscriptionCouponChanged = (previousCouponCode, nextCouponCode) => normalizeCouponCode(previousCouponCode) !==
18
19
  normalizeCouponCode(nextCouponCode);
@@ -25,7 +26,10 @@ export const invoicingSubscriptionSaveRequest = (rootState, id) => {
25
26
  // Shipping address captured via the shared Address screen is sent as an
26
27
  // object in the payload (same pattern as the customer save flow).
27
28
  const shippingAddress = rootState.addressViewState.newAddressState?.[INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE]?.addressToCreate;
28
- const data = localDataToSaveInvoicingSubscriptionPayload(localData, shippingAddress);
29
+ const flatFeePriceCodes = new Set(getInvoicingCatalogPrices(Object.values(rootState.invoicingCatalogState.itemByID))
30
+ .filter((price) => !invoicingPriceHasQuantity(price.pricingModel))
31
+ .flatMap((price) => price.priceCode ?? []));
32
+ const data = localDataToSaveInvoicingSubscriptionPayload(localData, shippingAddress, flatFeePriceCodes);
29
33
  if (id != null) {
30
34
  delete data.coupon_ids;
31
35
  }
@@ -23,19 +23,30 @@ const buildPlanQuery = (filters, cursor, sort) => {
23
23
  return params;
24
24
  };
25
25
  export const fetchInvoicingCatalogPlanListEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchInvoicingCatalogPlanList.match), switchMap((action) => {
26
- const { filters, cursor, keepExistingListItems, sort } = action.payload;
26
+ const { filters, cursor, fetchAll, keepExistingListItems, sort } = action.payload;
27
27
  const apiUrl = withQuery(`${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/catalog/plans`, buildPlanQuery(filters, cursor, sort));
28
28
  return zeniAPI.getJSON(apiUrl).pipe(switchMap((response) => {
29
29
  if (isSuccessResponse(response)) {
30
30
  const items = response.data?.items ?? [];
31
- return [
31
+ const nextCursor = response.data?.next_cursor ?? null;
32
+ const pageActions = [
32
33
  updateInvoicingCatalogItems(items, 'merge'),
33
34
  updateInvoicingCatalogPlanList({
34
35
  ids: items.map((item) => item.id),
35
- nextCursor: response.data?.next_cursor ?? null,
36
+ nextCursor,
36
37
  keepExistingListItems,
37
38
  }),
38
39
  ];
40
+ if (fetchAll && nextCursor != null && nextCursor !== cursor) {
41
+ pageActions.push(fetchInvoicingCatalogPlanList({
42
+ cursor: nextCursor,
43
+ fetchAll,
44
+ filters,
45
+ keepExistingListItems: true,
46
+ sort,
47
+ }));
48
+ }
49
+ return pageActions;
39
50
  }
40
51
  return of(updateInvoicingCatalogPlanListFailure(response.status));
41
52
  }), catchError((error) => of(updateInvoicingCatalogPlanListFailure(createZeniAPIStatus('Unexpected Error', 'Fetch catalog plan list REST API call errored out ' +