@zeniai/client-epic-state 5.2.42 → 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.
- package/lib/entity/invoicing/coupon/couponPayload.d.ts +8 -0
- package/lib/entity/invoicing/coupon/couponPayload.js +8 -0
- package/lib/entity/invoicing/coupon/couponState.d.ts +6 -0
- package/lib/esm/entity/invoicing/coupon/couponPayload.js +8 -0
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +40 -32
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.js +16 -1
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +29 -3
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.js +19 -0
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +17 -3
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.d.ts +30 -5
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +40 -32
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.d.ts +5 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.js +16 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.d.ts +23 -0
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +28 -2
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.d.ts +19 -0
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.js +21 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +17 -3
- package/package.json +2 -2
|
@@ -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,
|
|
@@ -3,7 +3,6 @@ import { toClassBasePayload } from '../../../entity/class/classPayload';
|
|
|
3
3
|
import { blankInvoicingCatalogSubFamily, isTieredCatalogPricingModel, } from './invoicingCatalogItemFormHelper';
|
|
4
4
|
const CURRENCY_CODE = 'USD';
|
|
5
5
|
const trimOrUndefined = (value) => value.trim() !== '' ? value.trim() : undefined;
|
|
6
|
-
const trimOrNull = (value) => value.trim() !== '' ? value.trim() : null;
|
|
7
6
|
const toProductCode = (value) => value
|
|
8
7
|
.trim()
|
|
9
8
|
.toLowerCase()
|
|
@@ -91,13 +90,24 @@ const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues, lookup) =
|
|
|
91
90
|
/** `unit` is the item's unit of measure, not a currency; `CURRENCY_CODE` above prices Price points. */
|
|
92
91
|
const DEFAULT_CATALOG_ITEM_UNIT = 'USD';
|
|
93
92
|
/**
|
|
94
|
-
* Build the parent catalog-item request body from the form draft.
|
|
95
|
-
*
|
|
93
|
+
* Build the parent catalog-item request body from the form draft. Every field
|
|
94
|
+
* the form holds is sent, so a value cleared on the form is cleared on the
|
|
95
|
+
* item: the service reads an absent key as "leave it alone", which is why
|
|
96
|
+
* `description` travels as an empty string and `applicable_items` as an empty
|
|
97
|
+
* list rather than being dropped. Fields the form does not collect
|
|
98
|
+
* (`external_name`, `item_family_id`, `metadata`, …) stay off the body so an
|
|
99
|
+
* edit cannot overwrite them with a value nobody chose.
|
|
96
100
|
*/
|
|
97
101
|
export const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate, lookup) => {
|
|
98
|
-
const description = trimOrUndefined(localData.description);
|
|
99
102
|
const productCode = trimOrUndefined(localData.productCode) ?? toProductCode(localData.name);
|
|
100
103
|
const itemPayload = {
|
|
104
|
+
// `applicable_items` only means anything while the item is restricted;
|
|
105
|
+
// sending the empty list otherwise is what clears a restriction that was
|
|
106
|
+
// just lifted.
|
|
107
|
+
applicable_items: localData.itemApplicability === 'restricted'
|
|
108
|
+
? localData.applicableItems
|
|
109
|
+
: [],
|
|
110
|
+
description: trimOrUndefined(localData.description) ?? '',
|
|
101
111
|
enabled_for_checkout: true,
|
|
102
112
|
enabled_in_portal: true,
|
|
103
113
|
is_giftable: false,
|
|
@@ -107,41 +117,39 @@ export const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate,
|
|
|
107
117
|
name: localData.name.trim(),
|
|
108
118
|
product_code: productCode,
|
|
109
119
|
type: localData.catalogType,
|
|
120
|
+
// The form no longer collects a unit; an item that already has one keeps it.
|
|
121
|
+
unit: trimOrUndefined(localData.unit) ?? DEFAULT_CATALOG_ITEM_UNIT,
|
|
110
122
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (localData.itemApplicability === 'restricted' &&
|
|
118
|
-
localData.applicableItems.length > 0) {
|
|
119
|
-
itemPayload.applicable_items = localData.applicableItems;
|
|
123
|
+
// The external id is assigned by the billing service and shown read-only, so
|
|
124
|
+
// it is echoed back only when the item already has one: sending the blank a
|
|
125
|
+
// create starts with would clear an id assigned in the meantime.
|
|
126
|
+
const externalId = trimOrUndefined(localData.externalId);
|
|
127
|
+
if (externalId != null) {
|
|
128
|
+
itemPayload.external_id = externalId;
|
|
120
129
|
}
|
|
121
130
|
const subFamilies = localData.subFamilies.length > 0
|
|
122
131
|
? localData.subFamilies
|
|
123
132
|
: [blankInvoicingCatalogSubFamily()];
|
|
124
|
-
itemPayload.
|
|
133
|
+
itemPayload.sub_families = isCreate
|
|
125
134
|
? subFamilies.map((subFamily) => buildSubFamilyPayload(localData, subFamily, false, lookup))
|
|
126
135
|
: subFamilies.map((subFamily) => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return id != null ? { ...detail, id } : detail;
|
|
136
|
+
// Edits send the same body a create does. The narrower hand-built one
|
|
137
|
+
// this replaces had to be kept in step by hand and was not: it dropped
|
|
138
|
+
// `accounting_detail` once, and `billing_cycles`, `price`,
|
|
139
|
+
// `pricing_model`, `period`, the trial fields, `proration_type`,
|
|
140
|
+
// `free_quantity`, `is_taxable` and `tiers` were never added at all, so
|
|
141
|
+
// editing any of them looked saved and changed nothing.
|
|
142
|
+
const detail = buildSubFamilyPayload(localData, subFamily, true, lookup);
|
|
143
|
+
// `null` rather than omitted when empty: an absent key on an update
|
|
144
|
+
// reads as "leave it alone", which would make a cleared mapping
|
|
145
|
+
// impossible to save — the same reason `description` and
|
|
146
|
+
// `invoice_notes` are cleared rather than dropped above.
|
|
147
|
+
detail.accounting_detail = detail.accounting_detail ?? null;
|
|
148
|
+
// A price point added on this form has no id yet, and travels with a
|
|
149
|
+
// blank one. The update endpoint reads that as "create this one", so it
|
|
150
|
+
// lands in the same request as the edits around it rather than a second
|
|
151
|
+
// call that could fail on its own.
|
|
152
|
+
return { ...detail, id: subFamily.id.trim() };
|
|
145
153
|
});
|
|
146
154
|
return itemPayload;
|
|
147
155
|
};
|
package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
};
|
package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.js
CHANGED
|
@@ -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
|
|
18
|
-
const
|
|
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
|
-
|
|
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,10 +1,30 @@
|
|
|
1
1
|
import { AccountBase } from '../../../entity/account/accountState';
|
|
2
2
|
import { ClassBase } from '../../../entity/class/classState';
|
|
3
3
|
import { InvoicingPlanPayload } from '../../../entity/invoicing/plan/planPayload';
|
|
4
|
-
import { InvoicingProductPayload } from '../../../entity/invoicing/product/productPayload';
|
|
4
|
+
import { InvoicingAccountingDetailPayload, InvoicingProductPayload } from '../../../entity/invoicing/product/productPayload';
|
|
5
5
|
import { ZeniAPIResponse } from '../../../responsePayload';
|
|
6
6
|
import { InvoicingCatalogItemFormLocalData } from './editInvoicingCatalogItemDetailViewState';
|
|
7
7
|
export type SaveInvoicingCatalogItemResponse = ZeniAPIResponse<InvoicingPlanPayload>;
|
|
8
|
+
/**
|
|
9
|
+
* The price-point half of a save body. `InvoicingProductPayload` is the shape
|
|
10
|
+
* the service returns; a save differs in two ways: a field is cleared by
|
|
11
|
+
* sending an explicit `null` (an absent key reads as "leave it alone"), and a
|
|
12
|
+
* price point the form has only just added carries a blank `id`, which the
|
|
13
|
+
* update endpoint reads as "create this one".
|
|
14
|
+
*/
|
|
15
|
+
export type InvoicingCatalogSubFamilySavePayload = Omit<Partial<InvoicingProductPayload>, 'accounting_detail' | 'description' | 'invoice_notes'> & {
|
|
16
|
+
accounting_detail?: InvoicingAccountingDetailPayload | null;
|
|
17
|
+
description?: string | null;
|
|
18
|
+
invoice_notes?: string | null;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The catalog-item save body: the item the service returns, minus the fields it
|
|
22
|
+
* assigns itself. Price points keep the `sub_families` name they are read back
|
|
23
|
+
* under, and differ only in allowing the cleared and blank-id forms above.
|
|
24
|
+
*/
|
|
25
|
+
export type InvoicingCatalogItemSavePayload = Omit<Partial<InvoicingPlanPayload>, 'sub_families'> & {
|
|
26
|
+
sub_families?: InvoicingCatalogSubFamilySavePayload[];
|
|
27
|
+
};
|
|
8
28
|
/**
|
|
9
29
|
* Resolves the QuickBooks ids the pickers submit back to the entities they name.
|
|
10
30
|
* The payload carries the whole account and class, so the ids alone are not
|
|
@@ -20,12 +40,17 @@ export interface InvoicingCatalogAccountingFields {
|
|
|
20
40
|
accountingCode: string;
|
|
21
41
|
sku: string;
|
|
22
42
|
}
|
|
23
|
-
export declare const buildAccountingDetail: (subFamily: InvoicingCatalogAccountingFields, lookup: InvoicingCatalogAccountingLookup) =>
|
|
43
|
+
export declare const buildAccountingDetail: (subFamily: InvoicingCatalogAccountingFields, lookup: InvoicingCatalogAccountingLookup) => InvoicingAccountingDetailPayload | undefined;
|
|
24
44
|
/**
|
|
25
|
-
* Build the parent catalog-item request body from the form draft.
|
|
26
|
-
*
|
|
45
|
+
* Build the parent catalog-item request body from the form draft. Every field
|
|
46
|
+
* the form holds is sent, so a value cleared on the form is cleared on the
|
|
47
|
+
* item: the service reads an absent key as "leave it alone", which is why
|
|
48
|
+
* `description` travels as an empty string and `applicable_items` as an empty
|
|
49
|
+
* list rather than being dropped. Fields the form does not collect
|
|
50
|
+
* (`external_name`, `item_family_id`, `metadata`, …) stay off the body so an
|
|
51
|
+
* edit cannot overwrite them with a value nobody chose.
|
|
27
52
|
*/
|
|
28
|
-
export declare const localDataToSaveInvoicingCatalogItemPayload: (localData: InvoicingCatalogItemFormLocalData, isCreate: boolean, lookup: InvoicingCatalogAccountingLookup) =>
|
|
53
|
+
export declare const localDataToSaveInvoicingCatalogItemPayload: (localData: InvoicingCatalogItemFormLocalData, isCreate: boolean, lookup: InvoicingCatalogAccountingLookup) => InvoicingCatalogItemSavePayload;
|
|
29
54
|
export type RunInvoicingCatalogPlanActionResponse = ZeniAPIResponse<InvoicingPlanPayload>;
|
|
30
55
|
export type RunInvoicingCatalogProductActionResponse = ZeniAPIResponse<InvoicingProductPayload>;
|
|
31
56
|
export type InvoicingCatalogItemDetailResponse = ZeniAPIResponse<InvoicingPlanPayload>;
|
|
@@ -6,7 +6,6 @@ const classPayload_1 = require("../../../entity/class/classPayload");
|
|
|
6
6
|
const invoicingCatalogItemFormHelper_1 = require("./invoicingCatalogItemFormHelper");
|
|
7
7
|
const CURRENCY_CODE = 'USD';
|
|
8
8
|
const trimOrUndefined = (value) => value.trim() !== '' ? value.trim() : undefined;
|
|
9
|
-
const trimOrNull = (value) => value.trim() !== '' ? value.trim() : null;
|
|
10
9
|
const toProductCode = (value) => value
|
|
11
10
|
.trim()
|
|
12
11
|
.toLowerCase()
|
|
@@ -95,13 +94,24 @@ const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues, lookup) =
|
|
|
95
94
|
/** `unit` is the item's unit of measure, not a currency; `CURRENCY_CODE` above prices Price points. */
|
|
96
95
|
const DEFAULT_CATALOG_ITEM_UNIT = 'USD';
|
|
97
96
|
/**
|
|
98
|
-
* Build the parent catalog-item request body from the form draft.
|
|
99
|
-
*
|
|
97
|
+
* Build the parent catalog-item request body from the form draft. Every field
|
|
98
|
+
* the form holds is sent, so a value cleared on the form is cleared on the
|
|
99
|
+
* item: the service reads an absent key as "leave it alone", which is why
|
|
100
|
+
* `description` travels as an empty string and `applicable_items` as an empty
|
|
101
|
+
* list rather than being dropped. Fields the form does not collect
|
|
102
|
+
* (`external_name`, `item_family_id`, `metadata`, …) stay off the body so an
|
|
103
|
+
* edit cannot overwrite them with a value nobody chose.
|
|
100
104
|
*/
|
|
101
105
|
const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate, lookup) => {
|
|
102
|
-
const description = trimOrUndefined(localData.description);
|
|
103
106
|
const productCode = trimOrUndefined(localData.productCode) ?? toProductCode(localData.name);
|
|
104
107
|
const itemPayload = {
|
|
108
|
+
// `applicable_items` only means anything while the item is restricted;
|
|
109
|
+
// sending the empty list otherwise is what clears a restriction that was
|
|
110
|
+
// just lifted.
|
|
111
|
+
applicable_items: localData.itemApplicability === 'restricted'
|
|
112
|
+
? localData.applicableItems
|
|
113
|
+
: [],
|
|
114
|
+
description: trimOrUndefined(localData.description) ?? '',
|
|
105
115
|
enabled_for_checkout: true,
|
|
106
116
|
enabled_in_portal: true,
|
|
107
117
|
is_giftable: false,
|
|
@@ -111,41 +121,39 @@ const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate, lookup)
|
|
|
111
121
|
name: localData.name.trim(),
|
|
112
122
|
product_code: productCode,
|
|
113
123
|
type: localData.catalogType,
|
|
124
|
+
// The form no longer collects a unit; an item that already has one keeps it.
|
|
125
|
+
unit: trimOrUndefined(localData.unit) ?? DEFAULT_CATALOG_ITEM_UNIT,
|
|
114
126
|
};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (localData.itemApplicability === 'restricted' &&
|
|
122
|
-
localData.applicableItems.length > 0) {
|
|
123
|
-
itemPayload.applicable_items = localData.applicableItems;
|
|
127
|
+
// The external id is assigned by the billing service and shown read-only, so
|
|
128
|
+
// it is echoed back only when the item already has one: sending the blank a
|
|
129
|
+
// create starts with would clear an id assigned in the meantime.
|
|
130
|
+
const externalId = trimOrUndefined(localData.externalId);
|
|
131
|
+
if (externalId != null) {
|
|
132
|
+
itemPayload.external_id = externalId;
|
|
124
133
|
}
|
|
125
134
|
const subFamilies = localData.subFamilies.length > 0
|
|
126
135
|
? localData.subFamilies
|
|
127
136
|
: [(0, invoicingCatalogItemFormHelper_1.blankInvoicingCatalogSubFamily)()];
|
|
128
|
-
itemPayload.
|
|
137
|
+
itemPayload.sub_families = isCreate
|
|
129
138
|
? subFamilies.map((subFamily) => buildSubFamilyPayload(localData, subFamily, false, lookup))
|
|
130
139
|
: subFamilies.map((subFamily) => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return id != null ? { ...detail, id } : detail;
|
|
140
|
+
// Edits send the same body a create does. The narrower hand-built one
|
|
141
|
+
// this replaces had to be kept in step by hand and was not: it dropped
|
|
142
|
+
// `accounting_detail` once, and `billing_cycles`, `price`,
|
|
143
|
+
// `pricing_model`, `period`, the trial fields, `proration_type`,
|
|
144
|
+
// `free_quantity`, `is_taxable` and `tiers` were never added at all, so
|
|
145
|
+
// editing any of them looked saved and changed nothing.
|
|
146
|
+
const detail = buildSubFamilyPayload(localData, subFamily, true, lookup);
|
|
147
|
+
// `null` rather than omitted when empty: an absent key on an update
|
|
148
|
+
// reads as "leave it alone", which would make a cleared mapping
|
|
149
|
+
// impossible to save — the same reason `description` and
|
|
150
|
+
// `invoice_notes` are cleared rather than dropped above.
|
|
151
|
+
detail.accounting_detail = detail.accounting_detail ?? null;
|
|
152
|
+
// A price point added on this form has no id yet, and travels with a
|
|
153
|
+
// blank one. The update endpoint reads that as "create this one", so it
|
|
154
|
+
// lands in the same request as the edits around it rather than a second
|
|
155
|
+
// call that could fail on its own.
|
|
156
|
+
return { ...detail, id: subFamily.id.trim() };
|
|
149
157
|
});
|
|
150
158
|
return itemPayload;
|
|
151
159
|
};
|
package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.d.ts
CHANGED
|
@@ -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>;
|
package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.js
CHANGED
|
@@ -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;
|
package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.d.ts
CHANGED
|
@@ -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
|
package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.d.ts
CHANGED
|
@@ -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;
|
package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.js
CHANGED
|
@@ -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
|
|
21
|
-
const
|
|
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
|
-
|
|
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.
|
|
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.
|
|
79
|
+
"vitest": "^4.1.11"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"@redux-devtools/extension": "^3.3.0",
|