@zeniai/client-epic-state 5.2.43 → 5.2.44-betaAK0

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.
@@ -1,3 +1,4 @@
1
+ import { AccountBasePayload } from '../../account/accountPayload';
1
2
  import { InvoicingCoupon } from './couponState';
2
3
  import { InvoicingCouponApplyOn, InvoicingCouponDurationType, InvoicingCouponPeriodUnit, InvoicingCouponStatus, InvoicingDiscountType } from './couponTypes';
3
4
  export interface InvoicingCouponPayload {
@@ -11,6 +12,13 @@ export interface InvoicingCouponPayload {
11
12
  discount_percentage?: number;
12
13
  discount_quantity?: number;
13
14
  discount_type?: InvoicingDiscountType;
15
+ /**
16
+ * The income account this coupon's discount line posts to. Stored as an
17
+ * `{account_id}` and hydrated on read, the same shape the QBO settings
18
+ * mappings use; `null` when the coupon has none and the settings default
19
+ * applies instead.
20
+ */
21
+ discounts_account?: Partial<AccountBasePayload> | null;
14
22
  duration_type?: InvoicingCouponDurationType;
15
23
  invoice_name?: string;
16
24
  invoice_notes?: string;
@@ -4,6 +4,7 @@ exports.mapInvoicingCouponPayloadToState = void 0;
4
4
  const amount_1 = require("../../../commonStateTypes/amount");
5
5
  const currencyHelper_1 = require("../../../commonStateTypes/currencyHelper");
6
6
  const zeniDayJS_1 = require("../../../zeniDayJS");
7
+ const invoicingCommonPayload_1 = require("../invoicingCommonPayload");
7
8
  // ── Payload → State mappers (snake_case wire shape → camelCase domain shape) ──
8
9
  const mapInvoicingCouponPayloadToState = (payload) => {
9
10
  const currencyCode = payload.currency_code ?? currencyHelper_1.defaultCustomerCurrency.currencyCode;
@@ -20,6 +21,13 @@ const mapInvoicingCouponPayloadToState = (payload) => {
20
21
  discountPercentage: payload.discount_percentage,
21
22
  discountQuantity: payload.discount_quantity,
22
23
  discountType: payload.discount_type,
24
+ // The QuickBooks id first, for the reason `firstSelectableInvoicingId`
25
+ // documents: the form's picker is keyed on it, so reading the internal id
26
+ // would leave the field blank over an account that is set.
27
+ discountsAccountId: (0, invoicingCommonPayload_1.firstSelectableInvoicingId)([
28
+ payload.discounts_account?.qbo_id,
29
+ payload.discounts_account?.account_id,
30
+ ]),
23
31
  durationType: payload.duration_type,
24
32
  invoiceName: payload.invoice_name,
25
33
  invoiceNotes: payload.invoice_notes,
@@ -11,6 +11,12 @@ export interface InvoicingCoupon {
11
11
  discountAmount?: Amount;
12
12
  discountPercentage?: number;
13
13
  discountQuantity?: number;
14
+ /**
15
+ * The income account for this coupon's discount line, as the id its picker
16
+ * submits. Absent when the coupon has none, which is what makes the QBO
17
+ * settings discounts account apply instead.
18
+ */
19
+ discountsAccountId?: ID;
14
20
  discountType?: InvoicingDiscountType;
15
21
  durationType?: InvoicingCouponDurationType;
16
22
  invoiceName?: string;
@@ -1,6 +1,7 @@
1
1
  import { toAmount } from '../../../commonStateTypes/amount';
2
2
  import { defaultCustomerCurrency } from '../../../commonStateTypes/currencyHelper';
3
3
  import { date } from '../../../zeniDayJS';
4
+ import { firstSelectableInvoicingId } from '../invoicingCommonPayload';
4
5
  // ── Payload → State mappers (snake_case wire shape → camelCase domain shape) ──
5
6
  export const mapInvoicingCouponPayloadToState = (payload) => {
6
7
  const currencyCode = payload.currency_code ?? defaultCustomerCurrency.currencyCode;
@@ -17,6 +18,13 @@ export const mapInvoicingCouponPayloadToState = (payload) => {
17
18
  discountPercentage: payload.discount_percentage,
18
19
  discountQuantity: payload.discount_quantity,
19
20
  discountType: payload.discount_type,
21
+ // The QuickBooks id first, for the reason `firstSelectableInvoicingId`
22
+ // documents: the form's picker is keyed on it, so reading the internal id
23
+ // would leave the field blank over an account that is set.
24
+ discountsAccountId: firstSelectableInvoicingId([
25
+ payload.discounts_account?.qbo_id,
26
+ payload.discounts_account?.account_id,
27
+ ]),
20
28
  durationType: payload.duration_type,
21
29
  invoiceName: payload.invoice_name,
22
30
  invoiceNotes: payload.invoice_notes,
@@ -1,9 +1,13 @@
1
+ import { toAccountPayload } from '../../../entity/account/accountPayload';
1
2
  /**
2
3
  * Build the create/update discount request body from the form draft (snake_case
3
4
  * matching the `/discounts` schema). On create the coupon id is part of the
4
5
  * body; on edit it is the URL param, so it is omitted here.
6
+ *
7
+ * `accountByQboId` resolves the discounts-account picker back to the account
8
+ * record the body carries, the same way the catalog and QBO settings bodies do.
5
9
  */
6
- export const localDataToSaveInvoicingCouponPayload = (localData, isCreate) => {
10
+ export const localDataToSaveInvoicingCouponPayload = (localData, isCreate, accountByQboId) => {
7
11
  const data = {
8
12
  apply_on: localData.applyOn,
9
13
  discount_type: localData.discountType,
@@ -54,5 +58,16 @@ export const localDataToSaveInvoicingCouponPayload = (localData, isCreate) => {
54
58
  if (invoiceNotes !== '') {
55
59
  data.invoice_notes = invoiceNotes;
56
60
  }
61
+ // Omitted rather than sent as `null` when there is none: the service reads a
62
+ // blank discounts account as "not given" and leaves what it has, which is
63
+ // what keeps a form that never showed the picker — QBO disconnected — from
64
+ // clearing an account set elsewhere.
65
+ const discountsAccountId = localData.discountsAccountId.trim();
66
+ const discountsAccount = discountsAccountId !== ''
67
+ ? accountByQboId?.(discountsAccountId)
68
+ : undefined;
69
+ if (discountsAccount != null) {
70
+ data.discounts_account = toAccountPayload(discountsAccount);
71
+ }
57
72
  return data;
58
73
  };
@@ -2,7 +2,9 @@ import { reduceAllFetchState } from '../../../commonStateTypes/reduceFetchState'
2
2
  import { getInvoicingCouponById } from '../../../entity/invoicing/coupon/couponSelector';
3
3
  import { toInvoicingFormOption, } from '../../../entity/invoicing/invoicingCommonPayload';
4
4
  import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
5
- import { initialInvoicingCouponFormLocalData, } from './editInvoicingCouponDetailViewState';
5
+ import { getInvoicingQboView, invoicingAccountsForQboAccountTypes, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
6
+ import { getInvoicingSettings } from '../settingsView/settingsViewSelector';
7
+ import { initialInvoicingCouponFormLocalData, invoicingCouponDiscountsAccountIdForCreate, } from './editInvoicingCouponDetailViewState';
6
8
  /**
7
9
  * Bundled view for the discount create/edit form: the save fetch state/error,
8
10
  * the id of the coupon last saved, and — when a `couponId` is passed (edit
@@ -45,6 +47,12 @@ export const getEditInvoicingCouponDetailViewState = (state, couponId) => {
45
47
  version: 1.0,
46
48
  };
47
49
  };
50
+ /**
51
+ * The account types the discounts slot falls back to when the config has no
52
+ * `discounts_account` kind. A QuickBooks discount line posts to an income
53
+ * account; offering every account instead would let a save be rejected.
54
+ */
55
+ const DEFAULT_DISCOUNTS_ACCOUNT_TYPES = 'Income';
48
56
  /**
49
57
  * Bundled view for the discount form screen: the edit view (save state + the
50
58
  * coupon to prefill in edit mode), the draft `localData` (seeded from the
@@ -60,14 +68,32 @@ export const getInvoicingCouponFormView = (state, couponId) => {
60
68
  const localData = couponId != null
61
69
  ? (editDraftById[couponId] ?? initialInvoicingCouponFormLocalData)
62
70
  : newCouponDraft;
63
- const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, } = getInvoicingConfigView(state);
71
+ const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, qboAccountMappingKindOptions, } = getInvoicingConfigView(state);
72
+ const isQboConnected = isInvoicingQboConnected(state);
73
+ const qboView = isQboConnected ? getInvoicingQboView(state) : undefined;
74
+ // The account types the discounts slot accepts come from the same config the
75
+ // QBO settings page reads, so the two pickers offer the same accounts and
76
+ // widening the slot stays a config change.
77
+ const discountsAccountTypes = qboAccountMappingKindOptions.find((kindOption) => kindOption.code === 'discounts_account')?.accountTypes ?? DEFAULT_DISCOUNTS_ACCOUNT_TYPES;
64
78
  return {
65
79
  ...base,
66
80
  applyOnOptions: discountApplyOnOptions.map(toInvoicingFormOption),
67
81
  discountTypeOptions: discountTypeOptions.map(toInvoicingFormOption),
68
82
  durationTypeOptions: discountDurationTypeOptions.map(toInvoicingFormOption),
69
- localData,
83
+ isQboConnected,
84
+ isQboPickerDataLoading: isInvoicingAccountingPickerDataLoading(state),
85
+ localData: couponId != null
86
+ ? localData
87
+ : {
88
+ ...localData,
89
+ // Seeded from settings only while the draft has none of its own, so
90
+ // the picker shows the account a create would post against before
91
+ // the user touches it, and stops seeding the moment they pick.
92
+ discountsAccountId: invoicingCouponDiscountsAccountIdForCreate(localData.discountsAccountId, getInvoicingSettings(state)?.qbo?.account_mappings),
93
+ },
70
94
  mode: couponId != null ? 'edit' : 'create',
71
95
  periodUnitOptions: billingPeriodUnitOptions.map(toInvoicingFormOption),
96
+ qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? [],
97
+ qboIncomeAccountIds: invoicingAccountsForQboAccountTypes(qboView?.accounts ?? [], discountsAccountTypes).map((account) => account.id),
72
98
  };
73
99
  };
@@ -1,9 +1,11 @@
1
+ import { getInvoicingQboAccountMappingId, } from '../../../entity/invoicing/invoicingCommonPayload';
1
2
  export const initialInvoicingCouponFormLocalData = {
2
3
  applyOn: 'invoice_amount',
3
4
  couponCode: '',
4
5
  couponId: '',
5
6
  discountPercentage: '',
6
7
  discountQuantity: '',
8
+ discountsAccountId: '',
7
9
  discountType: 'percentage',
8
10
  durationType: 'one_time',
9
11
  invoiceName: '',
@@ -29,6 +31,10 @@ export const entityToInvoicingCouponFormLocalData = (coupon) => {
29
31
  ? String(coupon.discountPercentage)
30
32
  : '',
31
33
  discountQuantity: coupon.discountQuantity != null ? String(coupon.discountQuantity) : '',
34
+ // The coupon's own account, never the current setting: an edit shows what
35
+ // this coupon will post against, which is what was stored when it was
36
+ // saved even if the settings default has moved since.
37
+ discountsAccountId: coupon.discountsAccountId ?? '',
32
38
  discountType: coupon.discountType ?? 'percentage',
33
39
  durationType: coupon.durationType ?? 'one_time',
34
40
  invoiceName: coupon.invoiceName ?? '',
@@ -41,3 +47,16 @@ export const entityToInvoicingCouponFormLocalData = (coupon) => {
41
47
  validTill: coupon.validTill != null ? coupon.validTill.format('YYYY-MM-DD') : '',
42
48
  };
43
49
  };
50
+ /**
51
+ * The discounts account a create starts from: the draft's own pick, or the QBO
52
+ * settings discounts account while the user has not made one. Read by both the
53
+ * form view and the save, so what the picker shows is what the request sends —
54
+ * a create that leaves the picker alone still stores the settings account on
55
+ * the coupon rather than relying on the setting staying put.
56
+ *
57
+ * Create only. An edit shows the coupon's stored account, so a coupon saved
58
+ * without one keeps showing none.
59
+ */
60
+ export const invoicingCouponDiscountsAccountIdForCreate = (discountsAccountId, accountMappings) => discountsAccountId.trim() !== ''
61
+ ? discountsAccountId.trim()
62
+ : (getInvoicingQboAccountMappingId(accountMappings ?? undefined, 'discounts_account') ?? '');
@@ -5,8 +5,11 @@ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiE
5
5
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
6
6
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
7
7
  import { addInvoicingCouponToList } from '../couponView/couponViewReducer';
8
+ import { invoicingQboAccountByQboId } from '../invoicingQboView/invoicingQboViewSelector';
9
+ import { getInvoicingSettings } from '../settingsView/settingsViewSelector';
8
10
  import { localDataToSaveInvoicingCouponPayload, } from './editInvoicingCouponDetailViewPayload';
9
11
  import { saveInvoicingCoupon, submitInvoicingCouponForm, submitInvoicingCouponFormFailure, submitInvoicingCouponFormSuccess, } from './editInvoicingCouponDetailViewReducer';
12
+ import { invoicingCouponDiscountsAccountIdForCreate } from './editInvoicingCouponDetailViewState';
10
13
  export const saveInvoicingCouponEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter((action) => submitInvoicingCouponForm.match(action) ||
11
14
  saveInvoicingCoupon.match(action)), mergeMap((action) => {
12
15
  const { id } = action.payload;
@@ -14,11 +17,22 @@ export const saveInvoicingCouponEpic = (actions$, state$, zeniAPI) => actions$.p
14
17
  const data = saveInvoicingCoupon.match(action)
15
18
  ? action.payload.data
16
19
  : (() => {
17
- const viewState = state$.value.editInvoicingCouponDetailViewState;
18
- const localData = (id != null
20
+ const state = state$.value;
21
+ const viewState = state.editInvoicingCouponDetailViewState;
22
+ const draft = (id != null
19
23
  ? viewState.editDraftById[id]
20
24
  : viewState.newCouponDraft) ?? viewState.newCouponDraft;
21
- return localDataToSaveInvoicingCouponPayload(localData, id == null);
25
+ // The create default is resolved here as well as in the form view,
26
+ // so a create that never touched the picker still persists the
27
+ // settings account on the coupon instead of leaving it to whatever
28
+ // the setting says the day an invoice is raised.
29
+ const localData = id == null
30
+ ? {
31
+ ...draft,
32
+ discountsAccountId: invoicingCouponDiscountsAccountIdForCreate(draft.discountsAccountId, getInvoicingSettings(state)?.qbo?.account_mappings),
33
+ }
34
+ : draft;
35
+ return localDataToSaveInvoicingCouponPayload(localData, id == null, (qboId) => invoicingQboAccountByQboId(state, qboId));
22
36
  })();
23
37
  const baseUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0`;
24
38
  const path = id != null
@@ -1,3 +1,4 @@
1
+ import { AccountBase } from '../../../entity/account/accountState';
1
2
  import { InvoicingCouponPayload } from '../../../entity/invoicing/coupon/couponPayload';
2
3
  import { ZeniAPIResponse } from '../../../responsePayload';
3
4
  import { InvoicingCouponFormLocalData } from './editInvoicingCouponDetailViewState';
@@ -6,7 +7,10 @@ export type InvoicingCouponWriteResponse = ZeniAPIResponse<InvoicingCouponPayloa
6
7
  * Build the create/update discount request body from the form draft (snake_case
7
8
  * matching the `/discounts` schema). On create the coupon id is part of the
8
9
  * body; on edit it is the URL param, so it is omitted here.
10
+ *
11
+ * `accountByQboId` resolves the discounts-account picker back to the account
12
+ * record the body carries, the same way the catalog and QBO settings bodies do.
9
13
  */
10
- export declare const localDataToSaveInvoicingCouponPayload: (localData: InvoicingCouponFormLocalData, isCreate: boolean) => Record<string, unknown>;
14
+ export declare const localDataToSaveInvoicingCouponPayload: (localData: InvoicingCouponFormLocalData, isCreate: boolean, accountByQboId?: (qboId: string) => AccountBase | undefined) => Record<string, unknown>;
11
15
  export type RunInvoicingDiscountActionResponse = ZeniAPIResponse<InvoicingCouponPayload>;
12
16
  export type InvoicingCouponDetailResponse = ZeniAPIResponse<InvoicingCouponPayload>;
@@ -1,12 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.localDataToSaveInvoicingCouponPayload = void 0;
4
+ const accountPayload_1 = require("../../../entity/account/accountPayload");
4
5
  /**
5
6
  * Build the create/update discount request body from the form draft (snake_case
6
7
  * matching the `/discounts` schema). On create the coupon id is part of the
7
8
  * body; on edit it is the URL param, so it is omitted here.
9
+ *
10
+ * `accountByQboId` resolves the discounts-account picker back to the account
11
+ * record the body carries, the same way the catalog and QBO settings bodies do.
8
12
  */
9
- const localDataToSaveInvoicingCouponPayload = (localData, isCreate) => {
13
+ const localDataToSaveInvoicingCouponPayload = (localData, isCreate, accountByQboId) => {
10
14
  const data = {
11
15
  apply_on: localData.applyOn,
12
16
  discount_type: localData.discountType,
@@ -57,6 +61,17 @@ const localDataToSaveInvoicingCouponPayload = (localData, isCreate) => {
57
61
  if (invoiceNotes !== '') {
58
62
  data.invoice_notes = invoiceNotes;
59
63
  }
64
+ // Omitted rather than sent as `null` when there is none: the service reads a
65
+ // blank discounts account as "not given" and leaves what it has, which is
66
+ // what keeps a form that never showed the picker — QBO disconnected — from
67
+ // clearing an account set elsewhere.
68
+ const discountsAccountId = localData.discountsAccountId.trim();
69
+ const discountsAccount = discountsAccountId !== ''
70
+ ? accountByQboId?.(discountsAccountId)
71
+ : undefined;
72
+ if (discountsAccount != null) {
73
+ data.discounts_account = (0, accountPayload_1.toAccountPayload)(discountsAccount);
74
+ }
60
75
  return data;
61
76
  };
62
77
  exports.localDataToSaveInvoicingCouponPayload = localDataToSaveInvoicingCouponPayload;
@@ -1,3 +1,4 @@
1
+ import { NestedAccountHierarchyForReport } from '../../../commonStateTypes/accountView/nestedAccountID';
1
2
  import { FetchState, FetchStateAndError, ID } from '../../../commonStateTypes/common';
2
3
  import { SelectorView } from '../../../commonStateTypes/viewAndReport/viewAndReport';
3
4
  import { InvoicingCoupon } from '../../../entity/invoicing/coupon/couponState';
@@ -42,9 +43,31 @@ export interface InvoicingCouponFormView extends EditInvoicingCouponDetailView {
42
43
  * Folded in here so the screen never reaches for the config view itself.
43
44
  */
44
45
  fieldHelp: InvoicingFieldHelpByCode;
46
+ /**
47
+ * False hides the discounts-account picker outright: with no QuickBooks
48
+ * connection there are no accounts to offer, and a coupon saves fine without
49
+ * one.
50
+ */
51
+ isQboConnected: boolean;
52
+ /**
53
+ * True while the accounting lists are still loading, so the discounts-account
54
+ * picker can show as busy rather than as an empty list.
55
+ */
56
+ isQboPickerDataLoading: boolean;
45
57
  localData: InvoicingCouponFormLocalData;
46
58
  mode: 'create' | 'edit';
47
59
  periodUnitOptions: InvoicingFormOption[];
60
+ /**
61
+ * The account tree the discounts-account picker renders, and the QuickBooks
62
+ * ids within it the discounts slot accepts. Two values rather than a filtered
63
+ * tree: the picker groups by parent, and dropping a parent that is not itself
64
+ * an income account would take its income children with it.
65
+ *
66
+ * Empty until QBO is connected — the form must not offer a mapping that
67
+ * cannot be pushed.
68
+ */
69
+ qboAccountHierarchy: NestedAccountHierarchyForReport[];
70
+ qboIncomeAccountIds: string[];
48
71
  }
49
72
  /**
50
73
  * Bundled view for the discount form screen: the edit view (save state + the
@@ -5,6 +5,8 @@ const reduceFetchState_1 = require("../../../commonStateTypes/reduceFetchState")
5
5
  const couponSelector_1 = require("../../../entity/invoicing/coupon/couponSelector");
6
6
  const invoicingCommonPayload_1 = require("../../../entity/invoicing/invoicingCommonPayload");
7
7
  const invoicingConfigViewSelector_1 = require("../invoicingConfigView/invoicingConfigViewSelector");
8
+ const invoicingQboViewSelector_1 = require("../invoicingQboView/invoicingQboViewSelector");
9
+ const settingsViewSelector_1 = require("../settingsView/settingsViewSelector");
8
10
  const editInvoicingCouponDetailViewState_1 = require("./editInvoicingCouponDetailViewState");
9
11
  /**
10
12
  * Bundled view for the discount create/edit form: the save fetch state/error,
@@ -49,6 +51,12 @@ const getEditInvoicingCouponDetailViewState = (state, couponId) => {
49
51
  };
50
52
  };
51
53
  exports.getEditInvoicingCouponDetailViewState = getEditInvoicingCouponDetailViewState;
54
+ /**
55
+ * The account types the discounts slot falls back to when the config has no
56
+ * `discounts_account` kind. A QuickBooks discount line posts to an income
57
+ * account; offering every account instead would let a save be rejected.
58
+ */
59
+ const DEFAULT_DISCOUNTS_ACCOUNT_TYPES = 'Income';
52
60
  /**
53
61
  * Bundled view for the discount form screen: the edit view (save state + the
54
62
  * coupon to prefill in edit mode), the draft `localData` (seeded from the
@@ -64,15 +72,33 @@ const getInvoicingCouponFormView = (state, couponId) => {
64
72
  const localData = couponId != null
65
73
  ? (editDraftById[couponId] ?? editInvoicingCouponDetailViewState_1.initialInvoicingCouponFormLocalData)
66
74
  : newCouponDraft;
67
- const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, } = (0, invoicingConfigViewSelector_1.getInvoicingConfigView)(state);
75
+ const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, qboAccountMappingKindOptions, } = (0, invoicingConfigViewSelector_1.getInvoicingConfigView)(state);
76
+ const isQboConnected = (0, invoicingQboViewSelector_1.isInvoicingQboConnected)(state);
77
+ const qboView = isQboConnected ? (0, invoicingQboViewSelector_1.getInvoicingQboView)(state) : undefined;
78
+ // The account types the discounts slot accepts come from the same config the
79
+ // QBO settings page reads, so the two pickers offer the same accounts and
80
+ // widening the slot stays a config change.
81
+ const discountsAccountTypes = qboAccountMappingKindOptions.find((kindOption) => kindOption.code === 'discounts_account')?.accountTypes ?? DEFAULT_DISCOUNTS_ACCOUNT_TYPES;
68
82
  return {
69
83
  ...base,
70
84
  applyOnOptions: discountApplyOnOptions.map(invoicingCommonPayload_1.toInvoicingFormOption),
71
85
  discountTypeOptions: discountTypeOptions.map(invoicingCommonPayload_1.toInvoicingFormOption),
72
86
  durationTypeOptions: discountDurationTypeOptions.map(invoicingCommonPayload_1.toInvoicingFormOption),
73
- localData,
87
+ isQboConnected,
88
+ isQboPickerDataLoading: (0, invoicingQboViewSelector_1.isInvoicingAccountingPickerDataLoading)(state),
89
+ localData: couponId != null
90
+ ? localData
91
+ : {
92
+ ...localData,
93
+ // Seeded from settings only while the draft has none of its own, so
94
+ // the picker shows the account a create would post against before
95
+ // the user touches it, and stops seeding the moment they pick.
96
+ discountsAccountId: (0, editInvoicingCouponDetailViewState_1.invoicingCouponDiscountsAccountIdForCreate)(localData.discountsAccountId, (0, settingsViewSelector_1.getInvoicingSettings)(state)?.qbo?.account_mappings),
97
+ },
74
98
  mode: couponId != null ? 'edit' : 'create',
75
99
  periodUnitOptions: billingPeriodUnitOptions.map(invoicingCommonPayload_1.toInvoicingFormOption),
100
+ qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? [],
101
+ qboIncomeAccountIds: (0, invoicingQboViewSelector_1.invoicingAccountsForQboAccountTypes)(qboView?.accounts ?? [], discountsAccountTypes).map((account) => account.id),
76
102
  };
77
103
  };
78
104
  exports.getInvoicingCouponFormView = getInvoicingCouponFormView;
@@ -1,6 +1,7 @@
1
1
  import { Amount } from '../../../commonStateTypes/amount';
2
2
  import { FetchStateAndError, FetchedState, ID } from '../../../commonStateTypes/common';
3
3
  import { InvoicingCoupon } from '../../../entity/invoicing/coupon/couponState';
4
+ import { InvoicingQboAccountMappingsPayload } from '../../../entity/invoicing/invoicingCommonPayload';
4
5
  /**
5
6
  * Draft form state for the create/edit discount-coupon form. Held in CES so the
6
7
  * form flows its data from here (mirrors the customer form pattern). Dates are
@@ -13,6 +14,13 @@ export interface InvoicingCouponFormLocalData {
13
14
  couponId: string;
14
15
  discountPercentage: string;
15
16
  discountQuantity: string;
17
+ /**
18
+ * The income account for this coupon's discount line, as the id its picker
19
+ * submits. Blank means "no account of its own", which is what makes the QBO
20
+ * settings discounts account apply instead — so on create it is seeded from
21
+ * that setting rather than left empty.
22
+ */
23
+ discountsAccountId: string;
16
24
  discountType: string;
17
25
  durationType: string;
18
26
  invoiceName: string;
@@ -38,3 +46,14 @@ export interface EditInvoicingCouponDetailViewState extends FetchedState {
38
46
  export declare const initialInvoicingCouponFormLocalData: InvoicingCouponFormLocalData;
39
47
  /** Seed the form draft from an existing coupon entity (edit mode). */
40
48
  export declare const entityToInvoicingCouponFormLocalData: (coupon?: InvoicingCoupon) => InvoicingCouponFormLocalData;
49
+ /**
50
+ * The discounts account a create starts from: the draft's own pick, or the QBO
51
+ * settings discounts account while the user has not made one. Read by both the
52
+ * form view and the save, so what the picker shows is what the request sends —
53
+ * a create that leaves the picker alone still stores the settings account on
54
+ * the coupon rather than relying on the setting staying put.
55
+ *
56
+ * Create only. An edit shows the coupon's stored account, so a coupon saved
57
+ * without one keeps showing none.
58
+ */
59
+ export declare const invoicingCouponDiscountsAccountIdForCreate: (discountsAccountId: string, accountMappings: InvoicingQboAccountMappingsPayload | null | undefined) => string;
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.entityToInvoicingCouponFormLocalData = exports.initialInvoicingCouponFormLocalData = void 0;
3
+ exports.invoicingCouponDiscountsAccountIdForCreate = exports.entityToInvoicingCouponFormLocalData = exports.initialInvoicingCouponFormLocalData = void 0;
4
+ const invoicingCommonPayload_1 = require("../../../entity/invoicing/invoicingCommonPayload");
4
5
  exports.initialInvoicingCouponFormLocalData = {
5
6
  applyOn: 'invoice_amount',
6
7
  couponCode: '',
7
8
  couponId: '',
8
9
  discountPercentage: '',
9
10
  discountQuantity: '',
11
+ discountsAccountId: '',
10
12
  discountType: 'percentage',
11
13
  durationType: 'one_time',
12
14
  invoiceName: '',
@@ -32,6 +34,10 @@ const entityToInvoicingCouponFormLocalData = (coupon) => {
32
34
  ? String(coupon.discountPercentage)
33
35
  : '',
34
36
  discountQuantity: coupon.discountQuantity != null ? String(coupon.discountQuantity) : '',
37
+ // The coupon's own account, never the current setting: an edit shows what
38
+ // this coupon will post against, which is what was stored when it was
39
+ // saved even if the settings default has moved since.
40
+ discountsAccountId: coupon.discountsAccountId ?? '',
35
41
  discountType: coupon.discountType ?? 'percentage',
36
42
  durationType: coupon.durationType ?? 'one_time',
37
43
  invoiceName: coupon.invoiceName ?? '',
@@ -45,3 +51,17 @@ const entityToInvoicingCouponFormLocalData = (coupon) => {
45
51
  };
46
52
  };
47
53
  exports.entityToInvoicingCouponFormLocalData = entityToInvoicingCouponFormLocalData;
54
+ /**
55
+ * The discounts account a create starts from: the draft's own pick, or the QBO
56
+ * settings discounts account while the user has not made one. Read by both the
57
+ * form view and the save, so what the picker shows is what the request sends —
58
+ * a create that leaves the picker alone still stores the settings account on
59
+ * the coupon rather than relying on the setting staying put.
60
+ *
61
+ * Create only. An edit shows the coupon's stored account, so a coupon saved
62
+ * without one keeps showing none.
63
+ */
64
+ const invoicingCouponDiscountsAccountIdForCreate = (discountsAccountId, accountMappings) => discountsAccountId.trim() !== ''
65
+ ? discountsAccountId.trim()
66
+ : ((0, invoicingCommonPayload_1.getInvoicingQboAccountMappingId)(accountMappings ?? undefined, 'discounts_account') ?? '');
67
+ exports.invoicingCouponDiscountsAccountIdForCreate = invoicingCouponDiscountsAccountIdForCreate;
@@ -8,8 +8,11 @@ const snackbarApiError_1 = require("../../../entity/snackbar/snackbarApiError");
8
8
  const snackbarReducer_1 = require("../../../entity/snackbar/snackbarReducer");
9
9
  const responsePayload_1 = require("../../../responsePayload");
10
10
  const couponViewReducer_1 = require("../couponView/couponViewReducer");
11
+ const invoicingQboViewSelector_1 = require("../invoicingQboView/invoicingQboViewSelector");
12
+ const settingsViewSelector_1 = require("../settingsView/settingsViewSelector");
11
13
  const editInvoicingCouponDetailViewPayload_1 = require("./editInvoicingCouponDetailViewPayload");
12
14
  const editInvoicingCouponDetailViewReducer_1 = require("./editInvoicingCouponDetailViewReducer");
15
+ const editInvoicingCouponDetailViewState_1 = require("./editInvoicingCouponDetailViewState");
13
16
  const saveInvoicingCouponEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)((action) => editInvoicingCouponDetailViewReducer_1.submitInvoicingCouponForm.match(action) ||
14
17
  editInvoicingCouponDetailViewReducer_1.saveInvoicingCoupon.match(action)), (0, operators_1.mergeMap)((action) => {
15
18
  const { id } = action.payload;
@@ -17,11 +20,22 @@ const saveInvoicingCouponEpic = (actions$, state$, zeniAPI) => actions$.pipe((0,
17
20
  const data = editInvoicingCouponDetailViewReducer_1.saveInvoicingCoupon.match(action)
18
21
  ? action.payload.data
19
22
  : (() => {
20
- const viewState = state$.value.editInvoicingCouponDetailViewState;
21
- const localData = (id != null
23
+ const state = state$.value;
24
+ const viewState = state.editInvoicingCouponDetailViewState;
25
+ const draft = (id != null
22
26
  ? viewState.editDraftById[id]
23
27
  : viewState.newCouponDraft) ?? viewState.newCouponDraft;
24
- return (0, editInvoicingCouponDetailViewPayload_1.localDataToSaveInvoicingCouponPayload)(localData, id == null);
28
+ // The create default is resolved here as well as in the form view,
29
+ // so a create that never touched the picker still persists the
30
+ // settings account on the coupon instead of leaving it to whatever
31
+ // the setting says the day an invoice is raised.
32
+ const localData = id == null
33
+ ? {
34
+ ...draft,
35
+ discountsAccountId: (0, editInvoicingCouponDetailViewState_1.invoicingCouponDiscountsAccountIdForCreate)(draft.discountsAccountId, (0, settingsViewSelector_1.getInvoicingSettings)(state)?.qbo?.account_mappings),
36
+ }
37
+ : draft;
38
+ return (0, editInvoicingCouponDetailViewPayload_1.localDataToSaveInvoicingCouponPayload)(localData, id == null, (qboId) => (0, invoicingQboViewSelector_1.invoicingQboAccountByQboId)(state, qboId));
25
39
  })();
26
40
  const baseUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0`;
27
41
  const path = id != null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.2.43",
3
+ "version": "5.2.44-betaAK0",
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",
@@ -76,7 +76,7 @@
76
76
  "semver": "^7.7.1",
77
77
  "typescript": "^5.7.3",
78
78
  "vite": "^7.3.5",
79
- "vitest": "^4.1.8"
79
+ "vitest": "^4.1.11"
80
80
  },
81
81
  "dependencies": {
82
82
  "@redux-devtools/extension": "^3.3.0",