@zeniai/client-epic-state 5.2.83 → 5.2.84-beta0UT

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 (40) hide show
  1. package/lib/esm/view/invoicing/createInvoice/epics/fetchCreateInvoiceFormPageEpic.js +19 -5
  2. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewPayload.js +7 -4
  3. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +3 -1
  4. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionDetailPageEpic.js +7 -4
  5. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionFormPageEpic.js +1 -0
  6. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/invoicingSubscriptionFormConfig.js +2 -0
  7. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +6 -2
  8. package/lib/esm/view/invoicing/invoicingCatalogListView/epics/fetchInvoicingCatalogPlanListEpic.js +14 -3
  9. package/lib/esm/view/invoicing/invoicingCatalogListView/epics/fetchInvoicingCatalogProductListEpic.js +14 -3
  10. package/lib/esm/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewReducer.js +2 -0
  11. package/lib/esm/view/invoicing/invoicingListTabCache.js +8 -0
  12. package/lib/esm/view/invoicing/issueCreditNote/issueCreditNotePayload.js +24 -9
  13. package/lib/esm/view/settingsView/epic/saveNotificationSettingsEpic.js +7 -0
  14. package/lib/esm/view/settingsView/settingsViewHelpers.js +1 -0
  15. package/lib/esm/view/settingsView/settingsViewReducer.js +16 -1
  16. package/lib/esm/view/settingsView/settingsViewState.js +1 -0
  17. package/lib/view/invoicing/createInvoice/epics/fetchCreateInvoiceFormPageEpic.js +18 -4
  18. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewPayload.d.ts +1 -1
  19. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewPayload.js +7 -4
  20. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.d.ts +2 -0
  21. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +2 -0
  22. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionDetailPageEpic.js +6 -3
  23. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionFormPageEpic.js +1 -0
  24. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/invoicingSubscriptionFormConfig.d.ts +2 -0
  25. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/invoicingSubscriptionFormConfig.js +4 -1
  26. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +5 -1
  27. package/lib/view/invoicing/invoicingCatalogListView/epics/fetchInvoicingCatalogPlanListEpic.js +14 -3
  28. package/lib/view/invoicing/invoicingCatalogListView/epics/fetchInvoicingCatalogProductListEpic.js +14 -3
  29. package/lib/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewReducer.d.ts +6 -0
  30. package/lib/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewReducer.js +2 -0
  31. package/lib/view/invoicing/invoicingListTabCache.d.ts +11 -1
  32. package/lib/view/invoicing/invoicingListTabCache.js +10 -1
  33. package/lib/view/invoicing/issueCreditNote/issueCreditNotePayload.js +24 -9
  34. package/lib/view/settingsView/epic/saveNotificationSettingsEpic.js +7 -0
  35. package/lib/view/settingsView/settingsViewHelpers.js +1 -0
  36. package/lib/view/settingsView/settingsViewPayload.d.ts +2 -0
  37. package/lib/view/settingsView/settingsViewReducer.js +16 -1
  38. package/lib/view/settingsView/settingsViewState.d.ts +2 -2
  39. package/lib/view/settingsView/settingsViewState.js +1 -0
  40. package/package.json +1 -1
@@ -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;
@@ -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 ' +
@@ -23,19 +23,30 @@ const buildProductQuery = (filters, cursor, sort) => {
23
23
  return params;
24
24
  };
25
25
  export const fetchInvoicingCatalogProductListEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchInvoicingCatalogProductList.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`, buildProductQuery(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
  updateInvoicingCatalogProductList({
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(fetchInvoicingCatalogProductList({
42
+ cursor: nextCursor,
43
+ fetchAll,
44
+ filters,
45
+ keepExistingListItems: true,
46
+ sort,
47
+ }));
48
+ }
49
+ return pageActions;
39
50
  }
40
51
  return of(updateInvoicingCatalogProductListFailure(response.status));
41
52
  }), catchError((error) => of(updateInvoicingCatalogProductListFailure(createZeniAPIStatus('Unexpected Error', 'Fetch catalog product list REST API call errored out ' +
@@ -36,6 +36,7 @@ const invoicingCatalogListView = createSlice({
36
36
  payload: {
37
37
  filters: payload.filters ?? {},
38
38
  cursor: payload.cursor,
39
+ fetchAll: payload.fetchAll ?? false,
39
40
  keepExistingListItems: payload.keepExistingListItems ?? false,
40
41
  sort: payload.sort,
41
42
  },
@@ -70,6 +71,7 @@ const invoicingCatalogListView = createSlice({
70
71
  payload: {
71
72
  filters: payload.filters ?? {},
72
73
  cursor: payload.cursor,
74
+ fetchAll: payload.fetchAll ?? false,
73
75
  keepExistingListItems: payload.keepExistingListItems ?? false,
74
76
  sort: payload.sort,
75
77
  },
@@ -33,6 +33,14 @@ export const isSameInvoicingListQuery = (a, b) => {
33
33
  a.sort?.field === b.sort?.field &&
34
34
  a.sort?.dir === b.sort?.dir);
35
35
  };
36
+ /**
37
+ * For a picker that searches client-side: reuse only a complete list fetched
38
+ * with the picker's own query, else load every page of it.
39
+ */
40
+ export const shouldFetchFullInvoicingList = (list, pickerFilters, cacheOverride) => cacheOverride ||
41
+ list.fetchState !== 'Completed' ||
42
+ list.nextCursor != null ||
43
+ !isSameInvoicingListQuery({ filters: list.filters, sort: list.sort }, { filters: pickerFilters });
36
44
  /**
37
45
  * Fetch a tab that is empty or failed; top up one that holds less than a page
38
46
  * while the server says there is more; leave a full or complete one cached.
@@ -32,17 +32,32 @@ export const localDataToIssueCreditNotePayload = (localData) => {
32
32
  ...(type !== '' ? { type } : {}),
33
33
  };
34
34
  };
35
+ /**
36
+ * The rate a unit was actually billed at. A plan or add-on line covering
37
+ * several periods carries the per-period `unit_amount` but the full term in
38
+ * `amount`, so only `amount` / `quantity` lets crediting every unit reach what
39
+ * the line billed.
40
+ */
41
+ const billedUnitAmount = (lineItem) => lineItem.quantity > 0
42
+ ? {
43
+ ...lineItem.amount,
44
+ amount: Math.round((lineItem.amount.amount / lineItem.quantity) * 100) / 100,
45
+ }
46
+ : lineItem.unitAmount;
35
47
  export const initializeIssueCreditNoteDraftFromInvoice = (invoice) => ({
36
48
  invoiceId: invoice.id,
37
49
  reasonCode: '',
38
50
  type: '',
39
- lineItems: (invoice.lineItems ?? []).map((lineItem) => ({
40
- description: lineItem.description,
41
- id: lineItem.id,
42
- maxQuantity: lineItem.quantity,
43
- maxUnitAmount: lineItem.unitAmount,
44
- quantity: String(lineItem.quantity),
45
- selected: true,
46
- unitAmount: lineItem.unitAmount,
47
- })),
51
+ lineItems: (invoice.lineItems ?? []).map((lineItem) => {
52
+ const unitAmount = billedUnitAmount(lineItem);
53
+ return {
54
+ description: lineItem.description,
55
+ id: lineItem.id,
56
+ maxQuantity: lineItem.quantity,
57
+ maxUnitAmount: unitAmount,
58
+ quantity: String(lineItem.quantity),
59
+ selected: true,
60
+ unitAmount,
61
+ };
62
+ }),
48
63
  });
@@ -50,6 +50,8 @@ function toSaveNotificationSettingsPayload(localData, notificationSettingsData)
50
50
  pending_bill_approval_notification: null,
51
51
  pending_reimbursement_approval_notification: null,
52
52
  is_pending_reimbursement_approval_notification_active: null,
53
+ is_pending_task_notification_active: null,
54
+ pending_task_notification: null,
53
55
  is_month_end_insights_active: monthEndInsightsNotificationSettings.enabled != null
54
56
  ? monthEndInsights.notify
55
57
  : null,
@@ -77,5 +79,10 @@ function toSaveNotificationSettingsPayload(localData, notificationSettingsData)
77
79
  0;
78
80
  payload.pending_reimbursement_approval_notification = toRemindersPayload(reminders.pendingReimbursementApprovalNotification);
79
81
  }
82
+ if (reminderNotificationSettings.pendingTaskNotification != null) {
83
+ payload.is_pending_task_notification_active =
84
+ reminders.pendingTaskNotification.preferredMode.length > 0;
85
+ payload.pending_task_notification = toRemindersPayload(reminders.pendingTaskNotification);
86
+ }
80
87
  return payload;
81
88
  }
@@ -140,6 +140,7 @@ export const isRemindersSectionVisibleOnSettingsPage = (reminderNotificationSett
140
140
  reminderNotificationSettings.pendingBillApprovalNotification != null ||
141
141
  reminderNotificationSettings.pendingReimbursementApprovalNotification !=
142
142
  null ||
143
+ reminderNotificationSettings.pendingTaskNotification != null ||
143
144
  reminderNotificationSettings.uncategorizedTransactionNotification != null);
144
145
  };
145
146
  export const isMonthEndNotificationsSectionVisibleOnSettingsPage = (monthEndInsightsNotificationSettings, signedInUseEmail) => {
@@ -4,6 +4,7 @@ import { toNotificationMode, toRemindersFrequencyType, } from './settingsViewSta
4
4
  const initialNotificationSettingsData = {
5
5
  reminderNotificationSettings: {
6
6
  missingReceiptNotification: null,
7
+ pendingTaskNotification: null,
7
8
  uncategorizedTransactionNotification: null,
8
9
  pendingBillApprovalNotification: null,
9
10
  pendingReimbursementApprovalNotification: null,
@@ -44,6 +45,7 @@ const initialNotificationSettingsLocalData = {
44
45
  missingReceiptNotification: initialRemindersLocalData,
45
46
  pendingBillApprovalNotification: initialRemindersLocalData,
46
47
  pendingReimbursementApprovalNotification: initialRemindersLocalData,
48
+ pendingTaskNotification: initialRemindersLocalData,
47
49
  uncategorizedTransactionNotification: initialRemindersLocalData,
48
50
  },
49
51
  };
@@ -373,7 +375,7 @@ function toRemindersLocalData(reminderNotificationSettingDetails) {
373
375
  }
374
376
  const toNotificationSettingsLocalData = (data, loggedInUserEmail) => {
375
377
  const { monthEndInsightsNotificationSettings, reminderNotificationSettings } = data;
376
- const { missingReceiptNotification, pendingBillApprovalNotification, pendingReimbursementApprovalNotification, uncategorizedTransactionNotification, } = reminderNotificationSettings;
378
+ const { missingReceiptNotification, pendingBillApprovalNotification, pendingReimbursementApprovalNotification, pendingTaskNotification, uncategorizedTransactionNotification, } = reminderNotificationSettings;
377
379
  return {
378
380
  monthEndInsights: {
379
381
  notify: monthEndInsightsNotificationSettings.enabled ??
@@ -384,10 +386,22 @@ const toNotificationSettingsLocalData = (data, loggedInUserEmail) => {
384
386
  missingReceiptNotification: toRemindersLocalData(missingReceiptNotification),
385
387
  pendingBillApprovalNotification: toRemindersLocalData(pendingBillApprovalNotification),
386
388
  pendingReimbursementApprovalNotification: toRemindersLocalData(pendingReimbursementApprovalNotification),
389
+ pendingTaskNotification: toRemindersLocalData(pendingTaskNotification),
387
390
  uncategorizedTransactionNotification: toRemindersLocalData(uncategorizedTransactionNotification),
388
391
  },
389
392
  };
390
393
  };
394
+ function toPendingTaskNotificationActive(data) {
395
+ if (data.is_pending_task_notification_active != null) {
396
+ return data.is_pending_task_notification_active;
397
+ }
398
+ // Rows saved before this flag existed store null. It shares the
399
+ // transactions-reminder scope with missing receipts and uncategorized
400
+ // transactions, so a set sibling means the user has access.
401
+ const hasTransactionReminderAccess = data.is_missing_receipt_notification_active != null ||
402
+ data.is_uncategorized_transaction_notification_active != null;
403
+ return hasTransactionReminderAccess ? false : null;
404
+ }
391
405
  function toReminderNotificationSettingsDetails(isNotificationActive, remindersPayload) {
392
406
  if (isNotificationActive != null) {
393
407
  return {
@@ -408,6 +422,7 @@ const toNotificationSettings = (data) => {
408
422
  missingReceiptNotification: toReminderNotificationSettingsDetails(data.is_missing_receipt_notification_active, data.missing_receipt_notification),
409
423
  pendingBillApprovalNotification: toReminderNotificationSettingsDetails(data.is_pending_bill_approval_notification_active, data.pending_bill_approval_notification),
410
424
  pendingReimbursementApprovalNotification: toReminderNotificationSettingsDetails(data.is_pending_reimbursement_approval_notification_active, data.pending_reimbursement_approval_notification),
425
+ pendingTaskNotification: toReminderNotificationSettingsDetails(toPendingTaskNotificationActive(data), data.pending_task_notification),
411
426
  uncategorizedTransactionNotification: toReminderNotificationSettingsDetails(data.is_uncategorized_transaction_notification_active, data.uncategorized_transaction_notification),
412
427
  },
413
428
  monthEndInsightsNotificationSettings: {
@@ -1,6 +1,7 @@
1
1
  import { stringToUnion } from '../../commonStateTypes/stringToUnion';
2
2
  export const REMINDER_NOTIFICATION_TYPES = [
3
3
  'missingReceiptNotification',
4
+ 'pendingTaskNotification',
4
5
  'uncategorizedTransactionNotification',
5
6
  'pendingBillApprovalNotification',
6
7
  'pendingReimbursementApprovalNotification',
@@ -16,6 +16,8 @@ const createInvoiceReducer_1 = require("../createInvoiceReducer");
16
16
  const shouldFetch = (fetchStateAndError, cacheOverride) => cacheOverride ||
17
17
  (fetchStateAndError.fetchState !== 'Completed' &&
18
18
  fetchStateAndError.fetchState !== 'In-Progress');
19
+ const PICKER_PLAN_FILTERS = { type: 'plan' };
20
+ const PICKER_PRODUCT_FILTERS = { type: 'addon|charge' };
19
21
  /**
20
22
  * Orchestrates the create-invoice form's option sources: on
21
23
  * `fetchCreateInvoiceFormPage`, fans out to the customer, catalog plan, catalog
@@ -37,15 +39,27 @@ const fetchCreateInvoiceFormPageEpic = (actions$, state$) => actions$.pipe((0, o
37
39
  tabId: invoicingListTabCache_1.INVOICING_LIST_ALL_TAB,
38
40
  }));
39
41
  }
40
- if (shouldFetch({ fetchState: catalogState.fetchState, error: catalogState.error }, cacheOverride)) {
42
+ if ((0, invoicingListTabCache_1.shouldFetchFullInvoicingList)({
43
+ fetchState: catalogState.fetchState,
44
+ filters: catalogState.planFilters,
45
+ nextCursor: catalogState.planNextCursor,
46
+ sort: catalogState.planSort,
47
+ }, PICKER_PLAN_FILTERS, cacheOverride)) {
41
48
  pageActions.push((0, invoicingCatalogListViewReducer_1.fetchInvoicingCatalogPlanList)({
42
- filters: { type: 'plan' },
49
+ fetchAll: true,
50
+ filters: PICKER_PLAN_FILTERS,
43
51
  keepExistingListItems: false,
44
52
  }));
45
53
  }
46
- if (shouldFetch(catalogState.productFetchState, cacheOverride)) {
54
+ if ((0, invoicingListTabCache_1.shouldFetchFullInvoicingList)({
55
+ fetchState: catalogState.productFetchState.fetchState,
56
+ filters: catalogState.productFilters,
57
+ nextCursor: catalogState.productNextCursor,
58
+ sort: catalogState.productSort,
59
+ }, PICKER_PRODUCT_FILTERS, cacheOverride)) {
47
60
  pageActions.push((0, invoicingCatalogListViewReducer_1.fetchInvoicingCatalogProductList)({
48
- filters: { type: 'addon|charge' },
61
+ fetchAll: true,
62
+ filters: PICKER_PRODUCT_FILTERS,
49
63
  keepExistingListItems: false,
50
64
  }));
51
65
  }
@@ -18,7 +18,7 @@ export type SaveInvoicingSubscriptionResponse = ZeniAPIResponse<InvoicingSubscri
18
18
  * Sending them made every price point whose cadence the form could not express
19
19
  * — a daily plan, for instance — fail with a 422.
20
20
  */
21
- export declare const localDataToSaveInvoicingSubscriptionPayload: (localData: InvoicingSubscriptionFormLocalData, shippingAddress?: AddressUpdatableInfo) => Record<string, unknown>;
21
+ export declare const localDataToSaveInvoicingSubscriptionPayload: (localData: InvoicingSubscriptionFormLocalData, shippingAddress?: AddressUpdatableInfo, flatFeePriceCodes?: ReadonlySet<string>) => Record<string, unknown>;
22
22
  export type RunInvoicingSubscriptionActionResponse = ZeniAPIResponse<InvoicingSubscriptionPayload>;
23
23
  /** Snake_case request bodies for subscription action endpoints. */
24
24
  export type ApplyCouponBody = {
@@ -20,22 +20,25 @@ const invoicingSubscriptionFormConfig_1 = require("./invoicingSubscriptionFormCo
20
20
  * Sending them made every price point whose cadence the form could not express
21
21
  * — a daily plan, for instance — fail with a 422.
22
22
  */
23
- const localDataToSaveInvoicingSubscriptionPayload = (localData, shippingAddress) => {
23
+ const localDataToSaveInvoicingSubscriptionPayload = (localData, shippingAddress, flatFeePriceCodes = new Set()) => {
24
+ const itemQuantity = (priceCode, quantity) => flatFeePriceCodes.has(priceCode)
25
+ ? 1
26
+ : (0, invoicingSubscriptionFormConfig_1.toInvoicingSubscriptionQuantity)(quantity);
24
27
  const items = [
25
28
  {
26
29
  item_price_code: localData.planPriceCode,
27
30
  item_type: 'plan',
28
- quantity: (0, invoicingSubscriptionFormConfig_1.toInvoicingSubscriptionQuantity)(localData.planQuantity),
31
+ quantity: itemQuantity(localData.planPriceCode, localData.planQuantity),
29
32
  },
30
33
  ...localData.addonPriceCodes.map((priceCode) => ({
31
34
  item_price_code: priceCode,
32
35
  item_type: 'addon',
33
- quantity: (0, invoicingSubscriptionFormConfig_1.toInvoicingSubscriptionQuantity)(localData.addonQuantities[priceCode]),
36
+ quantity: itemQuantity(priceCode, localData.addonQuantities[priceCode]),
34
37
  })),
35
38
  ...localData.chargePriceCodes.map((priceCode) => ({
36
39
  item_price_code: priceCode,
37
40
  item_type: 'charge',
38
- quantity: (0, invoicingSubscriptionFormConfig_1.toInvoicingSubscriptionQuantity)(localData.chargeQuantities[priceCode]),
41
+ quantity: itemQuantity(priceCode, localData.chargeQuantities[priceCode]),
39
42
  })),
40
43
  ];
41
44
  const setupFee = localData.setupFee?.amount;
@@ -147,6 +147,8 @@ export interface InvoicingSubscriptionOption {
147
147
  billingCycles?: number;
148
148
  billingPeriod?: number;
149
149
  billingPeriodUnit?: string;
150
+ /** Catalog options only: false for a flat fee, which takes no quantity. */
151
+ hasQuantity?: boolean;
150
152
  price?: Amount;
151
153
  productCode?: string;
152
154
  subtitle?: string;
@@ -182,6 +182,7 @@ const toInvoicingPlanOption = (plan) => ({
182
182
  // price point and rejects a subscription that claims a different one.
183
183
  billingPeriod: plan.period,
184
184
  billingPeriodUnit: plan.periodUnit,
185
+ hasQuantity: (0, invoicingSubscriptionFormConfig_1.invoicingPriceHasQuantity)(plan.pricingModel),
185
186
  label: plan.name ?? plan.id,
186
187
  price: invoicingUnitPrice(plan),
187
188
  productCode: plan.productCode,
@@ -193,6 +194,7 @@ const toInvoicingCatalogItemOption = (product) => ({
193
194
  // A one-time charge has no cadence, so its price is quoted flat rather than
194
195
  // per period.
195
196
  billingPeriodUnit: product.itemType === 'charge' ? undefined : product.periodUnit,
197
+ hasQuantity: (0, invoicingSubscriptionFormConfig_1.invoicingPriceHasQuantity)(product.pricingModel),
196
198
  label: product.name ?? product.id,
197
199
  price: invoicingUnitPrice(product),
198
200
  productCode: product.productCode,
@@ -43,11 +43,14 @@ const fetchInvoicingSubscriptionDetailPageEpic = (actions$, state$) => actions$.
43
43
  // Plan and coupon lists back the names the page renders for its line
44
44
  // items; both list slices cache, so this only refetches when asked to.
45
45
  const catalogListState = state.invoicingCatalogListViewState;
46
- if (shouldFetch({
47
- error: catalogListState.error,
46
+ if ((0, invoicingListTabCache_1.shouldFetchFullInvoicingList)({
48
47
  fetchState: catalogListState.fetchState,
49
- }, cacheOverride)) {
48
+ filters: catalogListState.planFilters,
49
+ nextCursor: catalogListState.planNextCursor,
50
+ sort: catalogListState.planSort,
51
+ }, { type: 'plan' }, cacheOverride)) {
50
52
  pageActions.push((0, invoicingCatalogListViewReducer_1.fetchInvoicingCatalogPlanList)({
53
+ fetchAll: true,
51
54
  filters: { type: 'plan' },
52
55
  keepExistingListItems: false,
53
56
  }));
@@ -23,6 +23,7 @@ const fetchInvoicingSubscriptionFormPageEpic = (actions$) => actions$.pipe((0, o
23
23
  // unfiltered list would replace the store's plans with every catalog
24
24
  // item.
25
25
  (0, invoicingCatalogListViewReducer_1.fetchInvoicingCatalogPlanList)({
26
+ fetchAll: true,
26
27
  filters: { type: 'plan' },
27
28
  keepExistingListItems: false,
28
29
  }),
@@ -8,6 +8,8 @@ export declare const INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE: "invoicing_su
8
8
  * their day gets their own calendar date rather than yesterday's.
9
9
  */
10
10
  export declare const invoicingSubscriptionTodayIso: () => string;
11
+ /** A flat fee bills its price once, so its quantity is always 1. */
12
+ export declare const invoicingPriceHasQuantity: (pricingModel?: string) => boolean;
11
13
  /**
12
14
  * Clamp a subscription-item quantity to the minimum the API accepts. Shared by
13
15
  * the plan, add-on and charge items so none of them can bill a 0 or a fraction.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.entityToInvoicingSubscriptionFormLocalData = exports.initialInvoicingSubscriptionFormLocalData = exports.toInvoicingSubscriptionBillingCycles = exports.toInvoicingSubscriptionQuantity = exports.invoicingSubscriptionTodayIso = exports.INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE = void 0;
3
+ exports.entityToInvoicingSubscriptionFormLocalData = exports.initialInvoicingSubscriptionFormLocalData = exports.toInvoicingSubscriptionBillingCycles = exports.toInvoicingSubscriptionQuantity = exports.invoicingPriceHasQuantity = exports.invoicingSubscriptionTodayIso = exports.INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE = void 0;
4
4
  const zeniDayJS_1 = require("../../../zeniDayJS");
5
5
  /** `AddressType` key used when the subscription form opens the shared Address screen. */
6
6
  exports.INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE = 'invoicing_subscription_shipping_address';
@@ -26,6 +26,9 @@ const subscriptionItemQuantities = (subscription, itemType) => {
26
26
  */
27
27
  const invoicingSubscriptionTodayIso = () => (0, zeniDayJS_1.dateNow)().format('YYYY-MM-DD');
28
28
  exports.invoicingSubscriptionTodayIso = invoicingSubscriptionTodayIso;
29
+ /** A flat fee bills its price once, so its quantity is always 1. */
30
+ const invoicingPriceHasQuantity = (pricingModel) => pricingModel !== 'flat_fee';
31
+ exports.invoicingPriceHasQuantity = invoicingPriceHasQuantity;
29
32
  /**
30
33
  * Clamp a subscription-item quantity to the minimum the API accepts. Shared by
31
34
  * the plan, add-on and charge items so none of them can bill a 0 or a fraction.
@@ -12,6 +12,7 @@ const responsePayload_1 = require("../../../responsePayload");
12
12
  const addressViewReducer_1 = require("../../addressView/addressViewReducer");
13
13
  const editInvoicingCustomerDetailViewReducer_1 = require("../editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer");
14
14
  const invoiceListReducer_1 = require("../invoiceList/invoiceListReducer");
15
+ const invoicingCatalogListViewSelector_1 = require("../invoicingCatalogListView/invoicingCatalogListViewSelector");
15
16
  const invoicingSubscriptionListViewReducer_1 = require("../invoicingSubscriptionListView/invoicingSubscriptionListViewReducer");
16
17
  const editInvoicingSubscriptionDetailViewPayload_1 = require("./editInvoicingSubscriptionDetailViewPayload");
17
18
  const editInvoicingSubscriptionDetailViewReducer_1 = require("./editInvoicingSubscriptionDetailViewReducer");
@@ -29,7 +30,10 @@ const invoicingSubscriptionSaveRequest = (rootState, id) => {
29
30
  // Shipping address captured via the shared Address screen is sent as an
30
31
  // object in the payload (same pattern as the customer save flow).
31
32
  const shippingAddress = rootState.addressViewState.newAddressState?.[invoicingSubscriptionFormConfig_1.INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE]?.addressToCreate;
32
- const data = (0, editInvoicingSubscriptionDetailViewPayload_1.localDataToSaveInvoicingSubscriptionPayload)(localData, shippingAddress);
33
+ const flatFeePriceCodes = new Set((0, invoicingCatalogListViewSelector_1.getInvoicingCatalogPrices)(Object.values(rootState.invoicingCatalogState.itemByID))
34
+ .filter((price) => !(0, invoicingSubscriptionFormConfig_1.invoicingPriceHasQuantity)(price.pricingModel))
35
+ .flatMap((price) => price.priceCode ?? []));
36
+ const data = (0, editInvoicingSubscriptionDetailViewPayload_1.localDataToSaveInvoicingSubscriptionPayload)(localData, shippingAddress, flatFeePriceCodes);
33
37
  if (id != null) {
34
38
  delete data.coupon_ids;
35
39
  }
@@ -26,19 +26,30 @@ const buildPlanQuery = (filters, cursor, sort) => {
26
26
  return params;
27
27
  };
28
28
  const fetchInvoicingCatalogPlanListEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(invoicingCatalogListViewReducer_1.fetchInvoicingCatalogPlanList.match), (0, operators_1.switchMap)((action) => {
29
- const { filters, cursor, keepExistingListItems, sort } = action.payload;
29
+ const { filters, cursor, fetchAll, keepExistingListItems, sort } = action.payload;
30
30
  const apiUrl = (0, invoicingApiHelper_1.withQuery)(`${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/catalog/plans`, buildPlanQuery(filters, cursor, sort));
31
31
  return zeniAPI.getJSON(apiUrl).pipe((0, operators_1.switchMap)((response) => {
32
32
  if ((0, responsePayload_1.isSuccessResponse)(response)) {
33
33
  const items = response.data?.items ?? [];
34
- return [
34
+ const nextCursor = response.data?.next_cursor ?? null;
35
+ const pageActions = [
35
36
  (0, invoicingCatalogReducer_1.updateInvoicingCatalogItems)(items, 'merge'),
36
37
  (0, invoicingCatalogListViewReducer_1.updateInvoicingCatalogPlanList)({
37
38
  ids: items.map((item) => item.id),
38
- nextCursor: response.data?.next_cursor ?? null,
39
+ nextCursor,
39
40
  keepExistingListItems,
40
41
  }),
41
42
  ];
43
+ if (fetchAll && nextCursor != null && nextCursor !== cursor) {
44
+ pageActions.push((0, invoicingCatalogListViewReducer_1.fetchInvoicingCatalogPlanList)({
45
+ cursor: nextCursor,
46
+ fetchAll,
47
+ filters,
48
+ keepExistingListItems: true,
49
+ sort,
50
+ }));
51
+ }
52
+ return pageActions;
42
53
  }
43
54
  return (0, rxjs_1.of)((0, invoicingCatalogListViewReducer_1.updateInvoicingCatalogPlanListFailure)(response.status));
44
55
  }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, invoicingCatalogListViewReducer_1.updateInvoicingCatalogPlanListFailure)((0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch catalog plan list REST API call errored out ' +
@@ -26,19 +26,30 @@ const buildProductQuery = (filters, cursor, sort) => {
26
26
  return params;
27
27
  };
28
28
  const fetchInvoicingCatalogProductListEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(invoicingCatalogListViewReducer_1.fetchInvoicingCatalogProductList.match), (0, operators_1.switchMap)((action) => {
29
- const { filters, cursor, keepExistingListItems, sort } = action.payload;
29
+ const { filters, cursor, fetchAll, keepExistingListItems, sort } = action.payload;
30
30
  const apiUrl = (0, invoicingApiHelper_1.withQuery)(`${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/catalog/plans`, buildProductQuery(filters, cursor, sort));
31
31
  return zeniAPI.getJSON(apiUrl).pipe((0, operators_1.switchMap)((response) => {
32
32
  if ((0, responsePayload_1.isSuccessResponse)(response)) {
33
33
  const items = response.data?.items ?? [];
34
- return [
34
+ const nextCursor = response.data?.next_cursor ?? null;
35
+ const pageActions = [
35
36
  (0, invoicingCatalogReducer_1.updateInvoicingCatalogItems)(items, 'merge'),
36
37
  (0, invoicingCatalogListViewReducer_1.updateInvoicingCatalogProductList)({
37
38
  ids: items.map((item) => item.id),
38
- nextCursor: response.data?.next_cursor ?? null,
39
+ nextCursor,
39
40
  keepExistingListItems,
40
41
  }),
41
42
  ];
43
+ if (fetchAll && nextCursor != null && nextCursor !== cursor) {
44
+ pageActions.push((0, invoicingCatalogListViewReducer_1.fetchInvoicingCatalogProductList)({
45
+ cursor: nextCursor,
46
+ fetchAll,
47
+ filters,
48
+ keepExistingListItems: true,
49
+ sort,
50
+ }));
51
+ }
52
+ return pageActions;
42
53
  }
43
54
  return (0, rxjs_1.of)((0, invoicingCatalogListViewReducer_1.updateInvoicingCatalogProductListFailure)(response.status));
44
55
  }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, invoicingCatalogListViewReducer_1.updateInvoicingCatalogProductListFailure)((0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch catalog product list REST API call errored out ' +
@@ -6,12 +6,15 @@ import { InvoicingCatalogListViewState, InvoicingCatalogPlanFilters, InvoicingCa
6
6
  export declare const initialState: InvoicingCatalogListViewState;
7
7
  export declare const fetchInvoicingCatalogPlanList: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[payload: {
8
8
  cursor?: string;
9
+ /** Keep requesting the next page until the list is complete. */
10
+ fetchAll?: boolean;
9
11
  filters?: InvoicingCatalogPlanFilters;
10
12
  keepExistingListItems?: boolean;
11
13
  sort?: InvoicingListSort;
12
14
  }], {
13
15
  filters: InvoicingCatalogPlanFilters;
14
16
  cursor: string | undefined;
17
+ fetchAll: boolean;
15
18
  keepExistingListItems: boolean;
16
19
  sort: InvoicingListSort | undefined;
17
20
  }, "invoicingCatalogListView/fetchInvoicingCatalogPlanList", never, never>, updateInvoicingCatalogPlanList: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
@@ -20,12 +23,15 @@ export declare const fetchInvoicingCatalogPlanList: import("@reduxjs/toolkit").A
20
23
  nextCursor: string | null;
21
24
  }, "invoicingCatalogListView/updateInvoicingCatalogPlanList">, updateInvoicingCatalogPlanListFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<ZeniAPIStatus<Record<string, unknown>>, "invoicingCatalogListView/updateInvoicingCatalogPlanListFailure">, fetchInvoicingCatalogProductList: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[payload: {
22
25
  cursor?: string;
26
+ /** Keep requesting the next page until the list is complete. */
27
+ fetchAll?: boolean;
23
28
  filters?: InvoicingCatalogProductFilters;
24
29
  keepExistingListItems?: boolean;
25
30
  sort?: InvoicingListSort;
26
31
  }], {
27
32
  filters: InvoicingCatalogProductFilters;
28
33
  cursor: string | undefined;
34
+ fetchAll: boolean;
29
35
  keepExistingListItems: boolean;
30
36
  sort: InvoicingListSort | undefined;
31
37
  }, "invoicingCatalogListView/fetchInvoicingCatalogProductList", never, never>, updateInvoicingCatalogProductList: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
@@ -40,6 +40,7 @@ const invoicingCatalogListView = (0, toolkit_1.createSlice)({
40
40
  payload: {
41
41
  filters: payload.filters ?? {},
42
42
  cursor: payload.cursor,
43
+ fetchAll: payload.fetchAll ?? false,
43
44
  keepExistingListItems: payload.keepExistingListItems ?? false,
44
45
  sort: payload.sort,
45
46
  },
@@ -74,6 +75,7 @@ const invoicingCatalogListView = (0, toolkit_1.createSlice)({
74
75
  payload: {
75
76
  filters: payload.filters ?? {},
76
77
  cursor: payload.cursor,
78
+ fetchAll: payload.fetchAll ?? false,
77
79
  keepExistingListItems: payload.keepExistingListItems ?? false,
78
80
  sort: payload.sort,
79
81
  },
@@ -1,4 +1,4 @@
1
- import { FetchStateAndError, ID } from '../../commonStateTypes/common';
1
+ import { FetchState, FetchStateAndError, ID } from '../../commonStateTypes/common';
2
2
  import { ZeniAPIStatus } from '../../responsePayload';
3
3
  import { InvoicingListSort } from './invoicingApiHelper';
4
4
  /**
@@ -42,6 +42,16 @@ export declare const isSameInvoicingListQuery: (a: {
42
42
  filters: object;
43
43
  sort?: InvoicingListSort;
44
44
  }) => boolean;
45
+ /**
46
+ * For a picker that searches client-side: reuse only a complete list fetched
47
+ * with the picker's own query, else load every page of it.
48
+ */
49
+ export declare const shouldFetchFullInvoicingList: (list: {
50
+ fetchState: FetchState;
51
+ filters: object;
52
+ nextCursor: string | null;
53
+ sort?: InvoicingListSort;
54
+ }, pickerFilters: object, cacheOverride: boolean) => boolean;
45
55
  /** What opening a tab should fetch, if anything. */
46
56
  export type InvoicingListTabFetch = {
47
57
  kind: 'none';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isSameInvoicingListQuery = exports.invoicingListStatusParam = exports.invoicingListFetchTab = exports.invoicingListSourceTab = exports.getInvoicingListTab = exports.INVOICING_LIST_ALL_TAB = void 0;
3
+ exports.shouldFetchFullInvoicingList = exports.isSameInvoicingListQuery = exports.invoicingListStatusParam = exports.invoicingListFetchTab = exports.invoicingListSourceTab = exports.getInvoicingListTab = exports.INVOICING_LIST_ALL_TAB = void 0;
4
4
  exports.planInvoicingListTabFetch = planInvoicingListTabFetch;
5
5
  exports.startInvoicingListTabFetch = startInvoicingListTabFetch;
6
6
  exports.completeInvoicingListTabFetch = completeInvoicingListTabFetch;
@@ -46,6 +46,15 @@ const isSameInvoicingListQuery = (a, b) => {
46
46
  a.sort?.dir === b.sort?.dir);
47
47
  };
48
48
  exports.isSameInvoicingListQuery = isSameInvoicingListQuery;
49
+ /**
50
+ * For a picker that searches client-side: reuse only a complete list fetched
51
+ * with the picker's own query, else load every page of it.
52
+ */
53
+ const shouldFetchFullInvoicingList = (list, pickerFilters, cacheOverride) => cacheOverride ||
54
+ list.fetchState !== 'Completed' ||
55
+ list.nextCursor != null ||
56
+ !(0, exports.isSameInvoicingListQuery)({ filters: list.filters, sort: list.sort }, { filters: pickerFilters });
57
+ exports.shouldFetchFullInvoicingList = shouldFetchFullInvoicingList;
49
58
  /**
50
59
  * Fetch a tab that is empty or failed; top up one that holds less than a page
51
60
  * while the server says there is more; leave a full or complete one cached.
@@ -37,18 +37,33 @@ const localDataToIssueCreditNotePayload = (localData) => {
37
37
  };
38
38
  };
39
39
  exports.localDataToIssueCreditNotePayload = localDataToIssueCreditNotePayload;
40
+ /**
41
+ * The rate a unit was actually billed at. A plan or add-on line covering
42
+ * several periods carries the per-period `unit_amount` but the full term in
43
+ * `amount`, so only `amount` / `quantity` lets crediting every unit reach what
44
+ * the line billed.
45
+ */
46
+ const billedUnitAmount = (lineItem) => lineItem.quantity > 0
47
+ ? {
48
+ ...lineItem.amount,
49
+ amount: Math.round((lineItem.amount.amount / lineItem.quantity) * 100) / 100,
50
+ }
51
+ : lineItem.unitAmount;
40
52
  const initializeIssueCreditNoteDraftFromInvoice = (invoice) => ({
41
53
  invoiceId: invoice.id,
42
54
  reasonCode: '',
43
55
  type: '',
44
- lineItems: (invoice.lineItems ?? []).map((lineItem) => ({
45
- description: lineItem.description,
46
- id: lineItem.id,
47
- maxQuantity: lineItem.quantity,
48
- maxUnitAmount: lineItem.unitAmount,
49
- quantity: String(lineItem.quantity),
50
- selected: true,
51
- unitAmount: lineItem.unitAmount,
52
- })),
56
+ lineItems: (invoice.lineItems ?? []).map((lineItem) => {
57
+ const unitAmount = billedUnitAmount(lineItem);
58
+ return {
59
+ description: lineItem.description,
60
+ id: lineItem.id,
61
+ maxQuantity: lineItem.quantity,
62
+ maxUnitAmount: unitAmount,
63
+ quantity: String(lineItem.quantity),
64
+ selected: true,
65
+ unitAmount,
66
+ };
67
+ }),
53
68
  });
54
69
  exports.initializeIssueCreditNoteDraftFromInvoice = initializeIssueCreditNoteDraftFromInvoice;
@@ -54,6 +54,8 @@ function toSaveNotificationSettingsPayload(localData, notificationSettingsData)
54
54
  pending_bill_approval_notification: null,
55
55
  pending_reimbursement_approval_notification: null,
56
56
  is_pending_reimbursement_approval_notification_active: null,
57
+ is_pending_task_notification_active: null,
58
+ pending_task_notification: null,
57
59
  is_month_end_insights_active: monthEndInsightsNotificationSettings.enabled != null
58
60
  ? monthEndInsights.notify
59
61
  : null,
@@ -81,5 +83,10 @@ function toSaveNotificationSettingsPayload(localData, notificationSettingsData)
81
83
  0;
82
84
  payload.pending_reimbursement_approval_notification = toRemindersPayload(reminders.pendingReimbursementApprovalNotification);
83
85
  }
86
+ if (reminderNotificationSettings.pendingTaskNotification != null) {
87
+ payload.is_pending_task_notification_active =
88
+ reminders.pendingTaskNotification.preferredMode.length > 0;
89
+ payload.pending_task_notification = toRemindersPayload(reminders.pendingTaskNotification);
90
+ }
84
91
  return payload;
85
92
  }
@@ -153,6 +153,7 @@ const isRemindersSectionVisibleOnSettingsPage = (reminderNotificationSettings) =
153
153
  reminderNotificationSettings.pendingBillApprovalNotification != null ||
154
154
  reminderNotificationSettings.pendingReimbursementApprovalNotification !=
155
155
  null ||
156
+ reminderNotificationSettings.pendingTaskNotification != null ||
156
157
  reminderNotificationSettings.uncategorizedTransactionNotification != null);
157
158
  };
158
159
  exports.isRemindersSectionVisibleOnSettingsPage = isRemindersSectionVisibleOnSettingsPage;
@@ -32,10 +32,12 @@ export interface NotificationSettingsPayload {
32
32
  is_month_end_insights_active: boolean | null;
33
33
  is_pending_bill_approval_notification_active: boolean | null;
34
34
  is_pending_reimbursement_approval_notification_active: boolean | null;
35
+ is_pending_task_notification_active: boolean | null;
35
36
  is_uncategorized_transaction_notification_active: boolean | null;
36
37
  missing_receipt_notification: RemindersPayload | null;
37
38
  pending_bill_approval_notification: RemindersPayload | null;
38
39
  pending_reimbursement_approval_notification: RemindersPayload | null;
40
+ pending_task_notification: RemindersPayload | null;
39
41
  uncategorized_transaction_notification: RemindersPayload | null;
40
42
  month_end_email_recipient_email_ids?: string[];
41
43
  task_checklist_preferences?: NotificationPreferencesPayload;
@@ -8,6 +8,7 @@ const settingsViewState_1 = require("./settingsViewState");
8
8
  const initialNotificationSettingsData = {
9
9
  reminderNotificationSettings: {
10
10
  missingReceiptNotification: null,
11
+ pendingTaskNotification: null,
11
12
  uncategorizedTransactionNotification: null,
12
13
  pendingBillApprovalNotification: null,
13
14
  pendingReimbursementApprovalNotification: null,
@@ -48,6 +49,7 @@ const initialNotificationSettingsLocalData = {
48
49
  missingReceiptNotification: initialRemindersLocalData,
49
50
  pendingBillApprovalNotification: initialRemindersLocalData,
50
51
  pendingReimbursementApprovalNotification: initialRemindersLocalData,
52
+ pendingTaskNotification: initialRemindersLocalData,
51
53
  uncategorizedTransactionNotification: initialRemindersLocalData,
52
54
  },
53
55
  };
@@ -377,7 +379,7 @@ function toRemindersLocalData(reminderNotificationSettingDetails) {
377
379
  }
378
380
  const toNotificationSettingsLocalData = (data, loggedInUserEmail) => {
379
381
  const { monthEndInsightsNotificationSettings, reminderNotificationSettings } = data;
380
- const { missingReceiptNotification, pendingBillApprovalNotification, pendingReimbursementApprovalNotification, uncategorizedTransactionNotification, } = reminderNotificationSettings;
382
+ const { missingReceiptNotification, pendingBillApprovalNotification, pendingReimbursementApprovalNotification, pendingTaskNotification, uncategorizedTransactionNotification, } = reminderNotificationSettings;
381
383
  return {
382
384
  monthEndInsights: {
383
385
  notify: monthEndInsightsNotificationSettings.enabled ??
@@ -388,10 +390,22 @@ const toNotificationSettingsLocalData = (data, loggedInUserEmail) => {
388
390
  missingReceiptNotification: toRemindersLocalData(missingReceiptNotification),
389
391
  pendingBillApprovalNotification: toRemindersLocalData(pendingBillApprovalNotification),
390
392
  pendingReimbursementApprovalNotification: toRemindersLocalData(pendingReimbursementApprovalNotification),
393
+ pendingTaskNotification: toRemindersLocalData(pendingTaskNotification),
391
394
  uncategorizedTransactionNotification: toRemindersLocalData(uncategorizedTransactionNotification),
392
395
  },
393
396
  };
394
397
  };
398
+ function toPendingTaskNotificationActive(data) {
399
+ if (data.is_pending_task_notification_active != null) {
400
+ return data.is_pending_task_notification_active;
401
+ }
402
+ // Rows saved before this flag existed store null. It shares the
403
+ // transactions-reminder scope with missing receipts and uncategorized
404
+ // transactions, so a set sibling means the user has access.
405
+ const hasTransactionReminderAccess = data.is_missing_receipt_notification_active != null ||
406
+ data.is_uncategorized_transaction_notification_active != null;
407
+ return hasTransactionReminderAccess ? false : null;
408
+ }
395
409
  function toReminderNotificationSettingsDetails(isNotificationActive, remindersPayload) {
396
410
  if (isNotificationActive != null) {
397
411
  return {
@@ -412,6 +426,7 @@ const toNotificationSettings = (data) => {
412
426
  missingReceiptNotification: toReminderNotificationSettingsDetails(data.is_missing_receipt_notification_active, data.missing_receipt_notification),
413
427
  pendingBillApprovalNotification: toReminderNotificationSettingsDetails(data.is_pending_bill_approval_notification_active, data.pending_bill_approval_notification),
414
428
  pendingReimbursementApprovalNotification: toReminderNotificationSettingsDetails(data.is_pending_reimbursement_approval_notification_active, data.pending_reimbursement_approval_notification),
429
+ pendingTaskNotification: toReminderNotificationSettingsDetails(toPendingTaskNotificationActive(data), data.pending_task_notification),
415
430
  uncategorizedTransactionNotification: toReminderNotificationSettingsDetails(data.is_uncategorized_transaction_notification_active, data.uncategorized_transaction_notification),
416
431
  },
417
432
  monthEndInsightsNotificationSettings: {
@@ -9,8 +9,8 @@ interface BillingAddress extends FetchStateAndError {
9
9
  export interface IntegrationsSettings {
10
10
  refreshQBO: FetchStateAndError;
11
11
  }
12
- export declare const REMINDER_NOTIFICATION_TYPES: readonly ["missingReceiptNotification", "uncategorizedTransactionNotification", "pendingBillApprovalNotification", "pendingReimbursementApprovalNotification"];
13
- declare const toReminderTypeKey: (v: string) => "missingReceiptNotification" | "uncategorizedTransactionNotification" | "pendingBillApprovalNotification" | "pendingReimbursementApprovalNotification";
12
+ export declare const REMINDER_NOTIFICATION_TYPES: readonly ["missingReceiptNotification", "pendingTaskNotification", "uncategorizedTransactionNotification", "pendingBillApprovalNotification", "pendingReimbursementApprovalNotification"];
13
+ declare const toReminderTypeKey: (v: string) => "missingReceiptNotification" | "pendingTaskNotification" | "uncategorizedTransactionNotification" | "pendingBillApprovalNotification" | "pendingReimbursementApprovalNotification";
14
14
  export type ReminderTypeKey = ReturnType<typeof toReminderTypeKey>;
15
15
  export declare const toRemindersFrequencyType: (v: string) => "weekly" | "monthly" | "daily" | "bi-weekly";
16
16
  export type FrequencyTypeKey = ReturnType<typeof toRemindersFrequencyType>;
@@ -4,6 +4,7 @@ exports.toNotificationMode = exports.NOTIFICATION_MODES = exports.toRemindersFre
4
4
  const stringToUnion_1 = require("../../commonStateTypes/stringToUnion");
5
5
  exports.REMINDER_NOTIFICATION_TYPES = [
6
6
  'missingReceiptNotification',
7
+ 'pendingTaskNotification',
7
8
  'uncategorizedTransactionNotification',
8
9
  'pendingBillApprovalNotification',
9
10
  'pendingReimbursementApprovalNotification',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.2.83",
3
+ "version": "5.2.84-beta0UT",
4
4
  "description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/esm/index.js",