@zeniai/client-epic-state 5.2.44 → 5.2.46
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/entity/invoicing/plan/planSelector.d.ts +11 -0
- package/lib/entity/invoicing/plan/planSelector.js +41 -0
- package/lib/entity/invoicing/product/productPayload.d.ts +7 -0
- package/lib/entity/invoicing/product/productPayload.js +3 -0
- package/lib/entity/invoicing/product/productState.d.ts +6 -0
- package/lib/esm/entity/invoicing/coupon/couponPayload.js +8 -0
- package/lib/esm/entity/invoicing/plan/planSelector.js +39 -0
- package/lib/esm/entity/invoicing/product/productPayload.js +3 -0
- package/lib/esm/index.js +1 -1
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.js +16 -1
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +68 -7
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.js +22 -0
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/fetchInvoicingCouponDetailPageEpic.js +22 -4
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +17 -3
- package/lib/index.d.ts +1 -1
- package/lib/index.js +3 -2
- 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 +32 -0
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +66 -5
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.d.ts +19 -0
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.js +24 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/fetchInvoicingCouponDetailPageEpic.d.ts +9 -5
- package/lib/view/invoicing/editInvoicingCouponDetailView/fetchInvoicingCouponDetailPageEpic.js +22 -4
- package/lib/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +17 -3
- package/package.json +1 -1
|
@@ -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,4 +1,15 @@
|
|
|
1
|
+
import { Amount } from '../../../commonStateTypes/amount';
|
|
1
2
|
import { ID } from '../../../commonStateTypes/common';
|
|
2
3
|
import { InvoicingPlan, InvoicingPlanState } from './planState';
|
|
3
4
|
export declare function getInvoicingPlansByIds(planState: InvoicingPlanState, ids: ID[]): InvoicingPlan[];
|
|
4
5
|
export declare function getInvoicingPlanById(planState: InvoicingPlanState, id: ID): InvoicingPlan | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* The lowest amount a catalog item can be bought at: the smallest single-unit
|
|
8
|
+
* price across its active price points.
|
|
9
|
+
*
|
|
10
|
+
* Undefined rather than a number when there is nothing to compare — no active
|
|
11
|
+
* price points, or none carrying an amount — and equally when the active price
|
|
12
|
+
* points are priced in different currencies, where the smallest number is not
|
|
13
|
+
* the lowest price. A zero is a real price and wins on its merits.
|
|
14
|
+
*/
|
|
15
|
+
export declare const getLowestInvoicingPlanPricePointAmount: (plan: InvoicingPlan | undefined) => Amount | undefined;
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getLowestInvoicingPlanPricePointAmount = void 0;
|
|
6
7
|
exports.getInvoicingPlansByIds = getInvoicingPlansByIds;
|
|
7
8
|
exports.getInvoicingPlanById = getInvoicingPlanById;
|
|
8
9
|
const get_1 = __importDefault(require("lodash/get"));
|
|
@@ -15,3 +16,43 @@ function getInvoicingPlansByIds(planState, ids) {
|
|
|
15
16
|
function getInvoicingPlanById(planState, id) {
|
|
16
17
|
return (0, get_1.default)(planState.planByID, id);
|
|
17
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* A price point counts while it is active. A missing status is treated as
|
|
21
|
+
* active: the list responses omit it for items that have never been archived.
|
|
22
|
+
*/
|
|
23
|
+
const isActiveInvoicingPricePoint = (pricePoint) => pricePoint.status == null || pricePoint.status === 'active';
|
|
24
|
+
/**
|
|
25
|
+
* The amount to show for one price point: what the service computed for a
|
|
26
|
+
* single unit. `price` is only a fallback for responses predating
|
|
27
|
+
* `display_price` — reading it first would show `0` for every tiered, volume
|
|
28
|
+
* and stairstep model, whose configured price is legitimately zero because the
|
|
29
|
+
* money lives in its tiers.
|
|
30
|
+
*/
|
|
31
|
+
const invoicingPricePointDisplayAmount = (pricePoint) => pricePoint.displayPrice ?? pricePoint.price;
|
|
32
|
+
/**
|
|
33
|
+
* The lowest amount a catalog item can be bought at: the smallest single-unit
|
|
34
|
+
* price across its active price points.
|
|
35
|
+
*
|
|
36
|
+
* Undefined rather than a number when there is nothing to compare — no active
|
|
37
|
+
* price points, or none carrying an amount — and equally when the active price
|
|
38
|
+
* points are priced in different currencies, where the smallest number is not
|
|
39
|
+
* the lowest price. A zero is a real price and wins on its merits.
|
|
40
|
+
*/
|
|
41
|
+
const getLowestInvoicingPlanPricePointAmount = (plan) => {
|
|
42
|
+
const amounts = (plan?.subFamilies ?? [])
|
|
43
|
+
.filter(isActiveInvoicingPricePoint)
|
|
44
|
+
.map(invoicingPricePointDisplayAmount)
|
|
45
|
+
.filter((amount) => amount != null);
|
|
46
|
+
if (amounts.length === 0) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const [firstAmount] = amounts;
|
|
50
|
+
if (amounts.some((amount) => amount.currencyCode !== firstAmount.currencyCode)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
// `reduce` keeps the earlier of two equal amounts, so a tie resolves to the
|
|
54
|
+
// price point listed first rather than to whichever the comparison happened
|
|
55
|
+
// to visit last.
|
|
56
|
+
return amounts.reduce((lowest, amount) => amount.amount < lowest.amount ? amount : lowest);
|
|
57
|
+
};
|
|
58
|
+
exports.getLowestInvoicingPlanPricePointAmount = getLowestInvoicingPlanPricePointAmount;
|
|
@@ -34,6 +34,13 @@ export interface InvoicingProductPayload {
|
|
|
34
34
|
currency_code?: string;
|
|
35
35
|
currency_symbol?: string;
|
|
36
36
|
description?: string;
|
|
37
|
+
/**
|
|
38
|
+
* What one unit of this price point costs, computed by the service. Read-only
|
|
39
|
+
* and never sent back: `price` stays the configured scalar the create/edit
|
|
40
|
+
* flows own, which for a tiered, volume or stairstep model is legitimately
|
|
41
|
+
* `0` because the money lives in `tiers`.
|
|
42
|
+
*/
|
|
43
|
+
display_price?: number;
|
|
37
44
|
enabled?: boolean;
|
|
38
45
|
enabled_for_checkout?: boolean;
|
|
39
46
|
enabled_in_portal?: boolean;
|
|
@@ -47,6 +47,9 @@ const mapInvoicingProductPayloadToState = (payload) => {
|
|
|
47
47
|
createdAt: payload.created_at != null ? (0, zeniDayJS_1.date)(payload.created_at) : undefined,
|
|
48
48
|
currencyCode: payload.currency_code,
|
|
49
49
|
description: payload.description,
|
|
50
|
+
displayPrice: payload.display_price != null
|
|
51
|
+
? (0, amount_1.toAmount)(payload.display_price, currencyCode, currencySymbol)
|
|
52
|
+
: undefined,
|
|
50
53
|
enabled: payload.enabled,
|
|
51
54
|
enabledForCheckout: payload.enabled_for_checkout,
|
|
52
55
|
enabledInPortal: payload.enabled_in_portal,
|
|
@@ -25,6 +25,12 @@ export interface InvoicingProduct {
|
|
|
25
25
|
createdAt?: ZeniDate;
|
|
26
26
|
currencyCode?: string;
|
|
27
27
|
description?: string;
|
|
28
|
+
/**
|
|
29
|
+
* What one unit costs, as the service computes it. Unlike {@link price} — the
|
|
30
|
+
* configured scalar, which a tiered model stores as `0` — this is the amount
|
|
31
|
+
* to show.
|
|
32
|
+
*/
|
|
33
|
+
displayPrice?: Amount;
|
|
28
34
|
enabled?: boolean;
|
|
29
35
|
enabledForCheckout?: boolean;
|
|
30
36
|
enabledInPortal?: boolean;
|
|
@@ -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,
|
|
@@ -8,3 +8,42 @@ export function getInvoicingPlansByIds(planState, ids) {
|
|
|
8
8
|
export function getInvoicingPlanById(planState, id) {
|
|
9
9
|
return recordGet(planState.planByID, id);
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* A price point counts while it is active. A missing status is treated as
|
|
13
|
+
* active: the list responses omit it for items that have never been archived.
|
|
14
|
+
*/
|
|
15
|
+
const isActiveInvoicingPricePoint = (pricePoint) => pricePoint.status == null || pricePoint.status === 'active';
|
|
16
|
+
/**
|
|
17
|
+
* The amount to show for one price point: what the service computed for a
|
|
18
|
+
* single unit. `price` is only a fallback for responses predating
|
|
19
|
+
* `display_price` — reading it first would show `0` for every tiered, volume
|
|
20
|
+
* and stairstep model, whose configured price is legitimately zero because the
|
|
21
|
+
* money lives in its tiers.
|
|
22
|
+
*/
|
|
23
|
+
const invoicingPricePointDisplayAmount = (pricePoint) => pricePoint.displayPrice ?? pricePoint.price;
|
|
24
|
+
/**
|
|
25
|
+
* The lowest amount a catalog item can be bought at: the smallest single-unit
|
|
26
|
+
* price across its active price points.
|
|
27
|
+
*
|
|
28
|
+
* Undefined rather than a number when there is nothing to compare — no active
|
|
29
|
+
* price points, or none carrying an amount — and equally when the active price
|
|
30
|
+
* points are priced in different currencies, where the smallest number is not
|
|
31
|
+
* the lowest price. A zero is a real price and wins on its merits.
|
|
32
|
+
*/
|
|
33
|
+
export const getLowestInvoicingPlanPricePointAmount = (plan) => {
|
|
34
|
+
const amounts = (plan?.subFamilies ?? [])
|
|
35
|
+
.filter(isActiveInvoicingPricePoint)
|
|
36
|
+
.map(invoicingPricePointDisplayAmount)
|
|
37
|
+
.filter((amount) => amount != null);
|
|
38
|
+
if (amounts.length === 0) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const [firstAmount] = amounts;
|
|
42
|
+
if (amounts.some((amount) => amount.currencyCode !== firstAmount.currencyCode)) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
// `reduce` keeps the earlier of two equal amounts, so a tie resolves to the
|
|
46
|
+
// price point listed first rather than to whichever the comparison happened
|
|
47
|
+
// to visit last.
|
|
48
|
+
return amounts.reduce((lowest, amount) => amount.amount < lowest.amount ? amount : lowest);
|
|
49
|
+
};
|
|
@@ -43,6 +43,9 @@ export const mapInvoicingProductPayloadToState = (payload) => {
|
|
|
43
43
|
createdAt: payload.created_at != null ? date(payload.created_at) : undefined,
|
|
44
44
|
currencyCode: payload.currency_code,
|
|
45
45
|
description: payload.description,
|
|
46
|
+
displayPrice: payload.display_price != null
|
|
47
|
+
? toAmount(payload.display_price, currencyCode, currencySymbol)
|
|
48
|
+
: undefined,
|
|
46
49
|
enabled: payload.enabled,
|
|
47
50
|
enabledForCheckout: payload.enabled_for_checkout,
|
|
48
51
|
enabledInPortal: payload.enabled_in_portal,
|
package/lib/esm/index.js
CHANGED
|
@@ -846,7 +846,7 @@ export { fetchInvoicingCatalogCounts, fetchInvoicingCatalogListPage, fetchInvoic
|
|
|
846
846
|
export { ALL_INVOICING_PRODUCT_SORT_KEYS, INVOICING_PRODUCT_COLUMNS, } from './view/invoicing/invoicingCatalogListView/invoicingCatalogListViewState';
|
|
847
847
|
export { getInvoicingCatalogListView } from './view/invoicing/invoicingCatalogListView/invoicingCatalogListViewSelector';
|
|
848
848
|
export { fetchInvoicingCatalogItemDetail } from './view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer';
|
|
849
|
-
export { getInvoicingPlanById } from './entity/invoicing/plan/planSelector';
|
|
849
|
+
export { getInvoicingPlanById, getLowestInvoicingPlanPricePointAmount, } from './entity/invoicing/plan/planSelector';
|
|
850
850
|
export { getInvoicingProductById } from './entity/invoicing/product/productSelector';
|
|
851
851
|
// ── Invoicing: settings ───────────────────────────────────────────────────────
|
|
852
852
|
export { connectInvoicingStripe, disconnectInvoicingStripe, fetchInvoicingSettings, resetInvoicingBrandingFormDraft, resetInvoicingQboSettingsDraft, resetSaveInvoicingSettings, saveInvoicingSettings, submitInvoicingBrandingForm, submitInvoicingQboSettings, updateInvoicingBrandingFormDraft, updateInvoicingQboSettingsDraft, updateInvoicingSettings, } from './view/invoicing/settingsView/settingsViewReducer';
|
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
|
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { reduceAllFetchState } from '../../../commonStateTypes/reduceFetchState';
|
|
2
2
|
import { getInvoicingCouponById } from '../../../entity/invoicing/coupon/couponSelector';
|
|
3
|
-
import { toInvoicingFormOption, } from '../../../entity/invoicing/invoicingCommonPayload';
|
|
3
|
+
import { toInvoicingFormOption, toInvoicingQboAccountMappingSlot, } from '../../../entity/invoicing/invoicingCommonPayload';
|
|
4
4
|
import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
|
|
5
|
-
import {
|
|
5
|
+
import { getInvoicingQboView, invoicingAccountsForQboAccountTypes, invoicingQboAccountByQboId, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
|
|
6
|
+
import { getInvoicingSettings, getInvoicingSettingsFetchState, } 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
|
|
@@ -23,6 +25,9 @@ export const getEditInvoicingCouponDetailViewState = (state, couponId) => {
|
|
|
23
25
|
fetchState: 'Not-Started',
|
|
24
26
|
})
|
|
25
27
|
: { error: undefined, fetchState: 'Completed' };
|
|
28
|
+
const coupon = couponId != null
|
|
29
|
+
? getInvoicingCouponById(state.invoicingCouponState, couponId)
|
|
30
|
+
: undefined;
|
|
26
31
|
return {
|
|
27
32
|
actionState: couponId != null
|
|
28
33
|
? (state.editInvoicingCouponDetailViewState.actionStateById[couponId] ??
|
|
@@ -36,15 +41,22 @@ export const getEditInvoicingCouponDetailViewState = (state, couponId) => {
|
|
|
36
41
|
{ error: undefined, fetchState: configFetchState },
|
|
37
42
|
]),
|
|
38
43
|
recordState,
|
|
39
|
-
coupon
|
|
40
|
-
|
|
41
|
-
: undefined,
|
|
44
|
+
coupon,
|
|
45
|
+
discountsAccountName: invoicingQboAccountByQboId(state, coupon?.discountsAccountId)?.accountName,
|
|
42
46
|
savedCouponId,
|
|
43
47
|
fetchState,
|
|
44
48
|
error,
|
|
45
49
|
version: 1.0,
|
|
46
50
|
};
|
|
47
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* The account types the discounts slot falls back to when the config has no
|
|
54
|
+
* `discounts_account` kind. A QuickBooks discount line posts to an income
|
|
55
|
+
* account; offering every account instead would let a save be rejected.
|
|
56
|
+
*/
|
|
57
|
+
const DEFAULT_DISCOUNTS_ACCOUNT_TYPES = 'Income';
|
|
58
|
+
/** The account-mapping slot a coupon's discounts account shares with settings. */
|
|
59
|
+
const DISCOUNTS_ACCOUNT_MAPPING_SLOT = 'discounts_account';
|
|
48
60
|
/**
|
|
49
61
|
* Bundled view for the discount form screen: the edit view (save state + the
|
|
50
62
|
* coupon to prefill in edit mode), the draft `localData` (seeded from the
|
|
@@ -60,14 +72,63 @@ export const getInvoicingCouponFormView = (state, couponId) => {
|
|
|
60
72
|
const localData = couponId != null
|
|
61
73
|
? (editDraftById[couponId] ?? initialInvoicingCouponFormLocalData)
|
|
62
74
|
: newCouponDraft;
|
|
63
|
-
const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, } = getInvoicingConfigView(state);
|
|
75
|
+
const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, qboAccountMappingKindOptions, } = getInvoicingConfigView(state);
|
|
76
|
+
const isQboConnected = isInvoicingQboConnected(state);
|
|
77
|
+
const qboView = isQboConnected ? getInvoicingQboView(state) : undefined;
|
|
78
|
+
const isQboPickerDataLoading = isInvoicingAccountingPickerDataLoading(state);
|
|
79
|
+
const settingsFetchState = getInvoicingSettingsFetchState(state).fetchState;
|
|
80
|
+
// Only a create reads the settings, and only until they arrive: an `Error`
|
|
81
|
+
// is a create that starts on no account, not a page that never renders.
|
|
82
|
+
const isCreateSeedPending = couponId == null &&
|
|
83
|
+
(settingsFetchState === 'Not-Started' ||
|
|
84
|
+
settingsFetchState === 'In-Progress');
|
|
85
|
+
// The account types the discounts slot accepts come from the same config the
|
|
86
|
+
// QBO settings page reads, so the two pickers offer the same accounts and
|
|
87
|
+
// widening the slot stays a config change. Matched on the slot rather than
|
|
88
|
+
// the code as written: a config that names the kind `discounts_account_id`
|
|
89
|
+
// is the same slot, and missing it here would quietly fall back to the
|
|
90
|
+
// default and offer a different set than the settings page does.
|
|
91
|
+
const discountsAccountTypes = qboAccountMappingKindOptions.find((kindOption) => toInvoicingQboAccountMappingSlot(kindOption.code) ===
|
|
92
|
+
DISCOUNTS_ACCOUNT_MAPPING_SLOT)?.accountTypes ?? DEFAULT_DISCOUNTS_ACCOUNT_TYPES;
|
|
64
93
|
return {
|
|
65
94
|
...base,
|
|
66
95
|
applyOnOptions: discountApplyOnOptions.map(toInvoicingFormOption),
|
|
67
96
|
discountTypeOptions: discountTypeOptions.map(toInvoicingFormOption),
|
|
68
97
|
durationTypeOptions: discountDurationTypeOptions.map(toInvoicingFormOption),
|
|
69
|
-
|
|
98
|
+
isQboConnected,
|
|
99
|
+
isQboPickerDataLoading,
|
|
100
|
+
// Widened past the record and config the detail page waits on: the form
|
|
101
|
+
// also renders from the account list, and seeds a create from the settings
|
|
102
|
+
// discounts account. A form that mounted before those landed would offer an
|
|
103
|
+
// empty picker and save a coupon with no account of its own — not wrong,
|
|
104
|
+
// since blank means "use the settings account", but not what the user was
|
|
105
|
+
// shown either. Widened here rather than on the base view, so the detail
|
|
106
|
+
// page does not start waiting on lists it never renders.
|
|
107
|
+
//
|
|
108
|
+
// Only while they are still coming, though, and the settings only on a
|
|
109
|
+
// create: a coupon saves fine without a discounts account, so neither is
|
|
110
|
+
// allowed to hold the form behind a skeleton once it has failed.
|
|
111
|
+
pageFetchState: reduceAllFetchState([
|
|
112
|
+
base.pageFetchState,
|
|
113
|
+
{
|
|
114
|
+
error: undefined,
|
|
115
|
+
fetchState: isQboPickerDataLoading || isCreateSeedPending
|
|
116
|
+
? 'In-Progress'
|
|
117
|
+
: 'Completed',
|
|
118
|
+
},
|
|
119
|
+
]),
|
|
120
|
+
localData: couponId != null
|
|
121
|
+
? localData
|
|
122
|
+
: {
|
|
123
|
+
...localData,
|
|
124
|
+
// Seeded from settings only while the draft has none of its own, so
|
|
125
|
+
// the picker shows the account a create would post against before
|
|
126
|
+
// the user touches it, and stops seeding the moment they pick.
|
|
127
|
+
discountsAccountId: invoicingCouponDiscountsAccountIdForCreate(localData.discountsAccountId, getInvoicingSettings(state)?.qbo?.account_mappings),
|
|
128
|
+
},
|
|
70
129
|
mode: couponId != null ? 'edit' : 'create',
|
|
71
130
|
periodUnitOptions: billingPeriodUnitOptions.map(toInvoicingFormOption),
|
|
131
|
+
qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? [],
|
|
132
|
+
qboIncomeAccountIds: invoicingAccountsForQboAccountTypes(qboView?.accounts ?? [], discountsAccountTypes).map((account) => account.id),
|
|
72
133
|
};
|
|
73
134
|
};
|
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,19 @@ 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) => {
|
|
61
|
+
const pickedAccountId = discountsAccountId.trim();
|
|
62
|
+
return pickedAccountId !== ''
|
|
63
|
+
? pickedAccountId
|
|
64
|
+
: (getInvoicingQboAccountMappingId(accountMappings ?? undefined, 'discounts_account') ?? '');
|
|
65
|
+
};
|
package/lib/esm/view/invoicing/editInvoicingCouponDetailView/fetchInvoicingCouponDetailPageEpic.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { EMPTY, from } from 'rxjs';
|
|
2
2
|
import { filter, mergeMap, withLatestFrom } from 'rxjs/operators';
|
|
3
3
|
import { fetchInvoicingConfig } from '../invoicingConfigView/invoicingConfigViewReducer';
|
|
4
|
+
import { fetchInvoicingAccountingPickers, fetchInvoicingQboConnection, } from '../invoicingQboView/invoicingQboViewReducer';
|
|
5
|
+
import { fetchInvoicingSettings } from '../settingsView/settingsViewReducer';
|
|
4
6
|
import { fetchInvoicingCouponDetail, fetchInvoicingCouponDetailPage, } from './editInvoicingCouponDetailViewReducer';
|
|
5
7
|
/**
|
|
6
8
|
* Fetch unless the endpoint is already loaded or in flight — or always when the
|
|
@@ -10,10 +12,12 @@ const shouldFetch = (fetchStateAndError, cacheOverride) => cacheOverride ||
|
|
|
10
12
|
(fetchStateAndError.fetchState !== 'Completed' &&
|
|
11
13
|
fetchStateAndError.fetchState !== 'In-Progress');
|
|
12
14
|
/**
|
|
13
|
-
* Orchestrates the discount
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Orchestrates the discount pages: one action fans out to the coupon record,
|
|
16
|
+
* the invoicing config behind its dropdowns and field help, and the QuickBooks
|
|
17
|
+
* connection, account list and settings the form's Discounts account picker
|
|
18
|
+
* renders and seeds from — each skipped when already cached. The page reads a
|
|
19
|
+
* single reduced fetch state off the view, so the form mounts once with real
|
|
20
|
+
* values.
|
|
17
21
|
*/
|
|
18
22
|
export const fetchInvoicingCouponDetailPageEpic = (actions$, state$) => actions$.pipe(filter(fetchInvoicingCouponDetailPage.match), withLatestFrom(state$), mergeMap(([action, state]) => {
|
|
19
23
|
const { cacheOverride = false, id } = action.payload;
|
|
@@ -32,5 +36,19 @@ export const fetchInvoicingCouponDetailPageEpic = (actions$, state$) => actions$
|
|
|
32
36
|
if (shouldFetch({ error: configState.error, fetchState: configState.fetchState }, cacheOverride)) {
|
|
33
37
|
pageActions.push(fetchInvoicingConfig());
|
|
34
38
|
}
|
|
39
|
+
const qboState = state.invoicingQboViewState;
|
|
40
|
+
if (shouldFetch(qboState.connectionFetchState, cacheOverride)) {
|
|
41
|
+
pageActions.push(fetchInvoicingQboConnection());
|
|
42
|
+
}
|
|
43
|
+
// The picker epic tops up the shared account list and skips what the
|
|
44
|
+
// store already holds, so this is safe to ask for on every open.
|
|
45
|
+
pageActions.push(fetchInvoicingAccountingPickers());
|
|
46
|
+
const settingsState = state.invoicingSettingsViewState;
|
|
47
|
+
if (shouldFetch({ error: settingsState.error, fetchState: settingsState.fetchState }, cacheOverride)) {
|
|
48
|
+
// A create seeds its Discounts account from the settings mapping, so
|
|
49
|
+
// the settings are page data here rather than a settings-screen
|
|
50
|
+
// concern.
|
|
51
|
+
pageActions.push(fetchInvoicingSettings());
|
|
52
|
+
}
|
|
35
53
|
return pageActions.length > 0 ? from(pageActions) : EMPTY;
|
|
36
54
|
}));
|
|
@@ -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
|
package/lib/index.d.ts
CHANGED
|
@@ -1233,7 +1233,7 @@ export { getInvoicingCatalogListView } from './view/invoicing/invoicingCatalogLi
|
|
|
1233
1233
|
export type { InvoicingCatalogListView } from './view/invoicing/invoicingCatalogListView/invoicingCatalogListViewSelector';
|
|
1234
1234
|
export { fetchInvoicingCatalogItemDetail } from './view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer';
|
|
1235
1235
|
export type { InvoicingCatalogItemKind } from './view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewState';
|
|
1236
|
-
export { getInvoicingPlanById } from './entity/invoicing/plan/planSelector';
|
|
1236
|
+
export { getInvoicingPlanById, getLowestInvoicingPlanPricePointAmount, } from './entity/invoicing/plan/planSelector';
|
|
1237
1237
|
export { getInvoicingProductById } from './entity/invoicing/product/productSelector';
|
|
1238
1238
|
export { connectInvoicingStripe, disconnectInvoicingStripe, fetchInvoicingSettings, resetInvoicingBrandingFormDraft, resetInvoicingQboSettingsDraft, resetSaveInvoicingSettings, saveInvoicingSettings, submitInvoicingBrandingForm, submitInvoicingQboSettings, updateInvoicingBrandingFormDraft, updateInvoicingQboSettingsDraft, updateInvoicingSettings, } from './view/invoicing/settingsView/settingsViewReducer';
|
|
1239
1239
|
export { getInvoicingBrandingFormView, getInvoicingQboSettingsFormView, getInvoicingSettings, getInvoicingSettingsFetchState, getInvoicingSettingsHubView, getInvoicingSettingsView, } from './view/invoicing/settingsView/settingsViewSelector';
|
package/lib/index.js
CHANGED
|
@@ -84,8 +84,8 @@ exports.initialInvoicingSubscriptionFormLocalData = exports.entityToInvoicingSub
|
|
|
84
84
|
exports.retryInvoicingMigration = exports.connectInvoicingChargebee = exports.getInvoicingDataImportView = exports.setInvoicingActiveMigrationSessionId = exports.fetchInvoicingMigrationSessions = exports.fetchInvoicingMigrationSession = exports.fetchInvoicingMigrationDiagnostics = exports.fetchInvoicingDataImportStatus = exports.runInvoicingPaymentAction = exports.runInvoicingDunningAction = exports.runInvoicingSubscriptionAction = exports.runInvoicingDiscountAction = exports.runInvoicingCatalogProductAction = exports.runInvoicingCatalogPlanAction = exports.fetchInvoicingCatalogItemDetailPage = exports.shouldDefaultAdjustmentCreditNote = exports.isBackdatedInvoiceDate = exports.buildVoidInvoiceSubmitBody = exports.updateInvoicingInvoice = exports.runInvoicingInvoiceAction = exports.getInvoicingCouponFormView = exports.getEditInvoicingCouponDetailViewState = exports.initialInvoicingCouponFormLocalData = exports.entityToInvoicingCouponFormLocalData = exports.updateInvoicingCouponFormDraft = exports.submitInvoicingCouponForm = exports.saveInvoicingCoupon = exports.resetEditInvoicingCouponDetailView = exports.fetchInvoicingCouponDetailPage = exports.getSavedInvoicingCatalogItemKind = exports.getSavedInvoicingCatalogItemId = exports.getInvoicingCatalogItemFormView = exports.getInvoicingApplicableCatalogItemOptions = exports.isTieredCatalogPricingModel = exports.initialInvoicingCatalogItemFormLocalData = exports.invoicingProductToCatalogItemFormSubFamily = exports.entityToInvoicingCatalogItemFormLocalData = exports.blankInvoicingCatalogTier = exports.blankInvoicingCatalogSubFamily = exports.INVOICING_CATALOG_TIER_TYPE_VALUES = exports.INVOICING_CATALOG_PRORATION_VALUES = exports.INVOICING_CATALOG_PRICING_MODEL_VALUES = exports.INVOICING_CATALOG_APPLICABILITY_VALUES = exports.updateInvoicingCatalogItemFormDraft = exports.submitInvoicingCatalogItemForm = exports.resetEditInvoicingCatalogItemDetailView = exports.getInvoicingSubscriptionFormView = exports.getEditInvoicingSubscriptionDetailViewState = exports.toInvoicingSubscriptionQuantity = exports.invoicingSubscriptionTodayIso = void 0;
|
|
85
85
|
exports.fetchInvoicingTransactionListPage = exports.fetchInvoicingTransactionList = exports.updateInvoicingTransactions = exports.removeInvoicingTransaction = exports.getInvoicingSubscriptionById = exports.fetchInvoicingSubscriptionDetailPage = exports.fetchInvoicingSubscriptionDetail = exports.getInvoicingSubscriptionListView = exports.INVOICING_SUBSCRIPTION_COLUMNS = exports.ALL_INVOICING_SUBSCRIPTION_TAB_IDS = exports.ALL_INVOICING_SUBSCRIPTION_SORT_KEYS = exports.updateInvoicingSubscriptionFilters = exports.setInvoicingSubscriptionActiveTab = exports.fetchInvoicingSubscriptionListPage = exports.fetchInvoicingSubscriptionList = exports.fetchInvoicingSubscriptionCounts = exports.updateInvoicingSubscriptions = exports.removeInvoicingSubscription = exports.resetPromotionalCreditAction = exports.submitAddPromotionalCredits = exports.getInvoicingSetupIntentFetchState = exports.getInvoicingSetupIntent = exports.getInvoicingSentPaymentLinkMagicUrl = exports.getInvoicingSentPaymentLinkEmail = exports.getInvoicingPlaidLinkToken = exports.getInvoicingPaymentMethodSaveState = exports.getInvoicingPaymentLinkSendState = exports.sendInvoicingPaymentLink = exports.saveInvoicingPaymentMethod = exports.resetInvoicingCustomerPaymentMethod = exports.fetchInvoicingPlaidLinkToken = exports.createInvoicingSetupIntent = exports.fetchInvoicingPaymentLinkEmailPreviewPage = exports.fetchInvoicingCustomerDetailPage = exports.fetchInvoicingCustomerDetail = exports.getInvoicingCustomerListView = exports.INVOICING_CUSTOMER_COLUMNS = exports.ALL_INVOICING_CUSTOMER_TAB_IDS = exports.ALL_INVOICING_CUSTOMER_SORT_KEYS = exports.updateInvoicingCustomerFilters = exports.setInvoicingCustomerActiveTab = exports.fetchInvoicingCustomerListPage = exports.fetchInvoicingCustomerList = exports.fetchInvoicingCustomerCounts = exports.updateInvoicingCustomers = exports.removeInvoicingCustomer = exports.INVOICING_LIST_PAGE_SIZE = exports.uploadInvoicingMigrationFiles = exports.startInvoicingMigration = exports.rollbackInvoicingMigration = void 0;
|
|
86
86
|
exports.ALL_INVOICING_DUNNING_CASE_SORT_KEYS = exports.updateInvoicingDunningCaseFilters = exports.setInvoicingDunningCaseActiveTab = exports.fetchInvoicingDunningCaseListPage = exports.fetchInvoicingDunningCaseList = exports.fetchInvoicingDunningCaseCounts = exports.updateInvoicingDunningCases = exports.removeInvoicingDunningCase = exports.fetchInvoicingCouponDetail = exports.getInvoicingCouponById = exports.getInvoicingCouponListView = exports.getInvoicingCouponListNextCursor = exports.getInvoicingCouponListItems = exports.getInvoicingCouponListHasMore = exports.getInvoicingCouponListFilters = exports.getInvoicingCouponListFetchState = exports.getInvoicingCouponCounts = exports.INVOICING_COUPON_COLUMNS = exports.ALL_INVOICING_COUPON_TAB_IDS = exports.ALL_INVOICING_COUPON_SORT_KEYS = exports.updateInvoicingCouponFilters = exports.setInvoicingCouponActiveTab = exports.fetchInvoicingCouponListPage = exports.fetchInvoicingCouponList = exports.fetchInvoicingCouponCounts = exports.updateInvoicingCoupons = exports.removeInvoicingCoupon = exports.getInvoicingCreditNoteDetail = exports.getInvoicingCreditNoteById = exports.fetchInvoicingCreditNoteDetail = exports.getInvoicingCreditNoteListView = exports.INVOICING_CREDIT_NOTE_COLUMNS = exports.ALL_INVOICING_CREDIT_NOTE_TAB_IDS = exports.ALL_INVOICING_CREDIT_NOTE_SORT_KEYS = exports.updateInvoicingCreditNoteFilters = exports.setInvoicingCreditNoteActiveTab = exports.fetchInvoicingCreditNoteListPage = exports.fetchInvoicingCreditNoteList = exports.fetchInvoicingCreditNoteCounts = exports.updateInvoicingCreditNotes = exports.removeInvoicingCreditNote = exports.getInvoicingTransactionDetail = exports.getInvoicingTransactionById = exports.fetchInvoicingTransactionDetail = exports.getInvoicingTransactionListView = exports.INVOICING_TRANSACTION_COLUMNS = exports.ALL_INVOICING_TRANSACTION_TAB_IDS = exports.ALL_INVOICING_TRANSACTION_SORT_KEYS = exports.updateInvoicingTransactionFilters = exports.setInvoicingTransactionActiveTab = void 0;
|
|
87
|
-
exports.
|
|
88
|
-
exports.isPendingReviewAccrual = exports.isManuallyReversedAccrual = exports.isAutoReversedAccrual = exports.getExpenseAutomationActualMatchingView = exports.getExpenseAutomationActualMatchingMatchView = exports.parseActualMatchingMatchId = exports.getCloseMatchAfterUndo = exports.getActualMatchingReversalDate = exports.getActualMatchingAmount = exports.isPendingReviewRow = exports.isRenderableOneTimeAccrual = exports.isOneTimeAccrualRow = exports.isExistingTabRow = exports.isAiAccountantSchedule = exports.isAccruedScheduleRow = exports.getJEScheduleRowType = exports.getExpenseAutomationJEOneTimeAccrualDetailView = exports.getInvoicingOnboardingGateView = exports.getInvoicingConfigView = exports.updateInvoicingConfig = exports.resetInvoicingConfigActionStates = exports.fetchInvoicingConfig = exports.enableInvoicing = exports.acceptInvoicingTerms = exports.toInvoicingQboCustomerMatchingMode = exports.toInvoicingNamedOptionsByKey = exports.getInvoicingQboAccountMappingId = exports.INVOICING_QBO_CUSTOMER_MATCHING_MODES = exports.getInvoicingQboView = exports.invoicingAccountsForQboAccountTypes = exports.startInvoicingQboBackfill = exports.retryInvoicingQboSyncTask = exports.fetchInvoicingQboSyncHealth = exports.fetchInvoicingQboConnection = exports.fetchInvoicingAccountingPickers = exports.isStripeConnectedFromSettings = exports.initialInvoicingBrandingFormLocalData = exports.entityToInvoicingBrandingFormLocalData = void 0;
|
|
87
|
+
exports.invoicingQboSettingsLocalDataToPayload = exports.initialInvoicingQboSettingsLocalData = exports.entityToInvoicingQboSettingsLocalData = exports.getInvoicingSettingsView = exports.getInvoicingSettingsHubView = exports.getInvoicingSettingsFetchState = exports.getInvoicingSettings = exports.getInvoicingQboSettingsFormView = exports.getInvoicingBrandingFormView = exports.updateInvoicingSettings = exports.updateInvoicingQboSettingsDraft = exports.updateInvoicingBrandingFormDraft = exports.submitInvoicingQboSettings = exports.submitInvoicingBrandingForm = exports.saveInvoicingSettings = exports.resetSaveInvoicingSettings = exports.resetInvoicingQboSettingsDraft = exports.resetInvoicingBrandingFormDraft = exports.fetchInvoicingSettings = exports.disconnectInvoicingStripe = exports.connectInvoicingStripe = exports.getInvoicingProductById = exports.getLowestInvoicingPlanPricePointAmount = exports.getInvoicingPlanById = exports.fetchInvoicingCatalogItemDetail = exports.getInvoicingCatalogListView = exports.INVOICING_PRODUCT_COLUMNS = exports.ALL_INVOICING_PRODUCT_SORT_KEYS = exports.updateInvoicingCatalogProductFilters = exports.updateInvoicingCatalogPlanFilters = exports.fetchInvoicingCatalogProductList = exports.fetchInvoicingCatalogPlanList = exports.fetchInvoicingCatalogListPage = exports.fetchInvoicingCatalogCounts = exports.updateInvoicingProducts = exports.removeInvoicingProduct = exports.updateInvoicingPlans = exports.removeInvoicingPlan = exports.mergeInvoicingPlanSubFamily = exports.getInvoicingDunningEmailPreview = exports.getInvoicingDunningCaseDetail = exports.getInvoicingDunningCaseById = exports.invalidateInvoicingDunningEmailPreview = exports.fetchInvoicingDunningEmailPreviewPage = exports.fetchInvoicingDunningCaseDetailPage = exports.fetchInvoicingDunningCaseDetail = exports.clearInvoicingDunningCaseDetailView = exports.getInvoicingDunningCaseListView = exports.INVOICING_DUNNING_CASE_COLUMNS = exports.ALL_INVOICING_DUNNING_CASE_TAB_IDS = void 0;
|
|
88
|
+
exports.isPendingReviewAccrual = exports.isManuallyReversedAccrual = exports.isAutoReversedAccrual = exports.getExpenseAutomationActualMatchingView = exports.getExpenseAutomationActualMatchingMatchView = exports.parseActualMatchingMatchId = exports.getCloseMatchAfterUndo = exports.getActualMatchingReversalDate = exports.getActualMatchingAmount = exports.isPendingReviewRow = exports.isRenderableOneTimeAccrual = exports.isOneTimeAccrualRow = exports.isExistingTabRow = exports.isAiAccountantSchedule = exports.isAccruedScheduleRow = exports.getJEScheduleRowType = exports.getExpenseAutomationJEOneTimeAccrualDetailView = exports.getInvoicingOnboardingGateView = exports.getInvoicingConfigView = exports.updateInvoicingConfig = exports.resetInvoicingConfigActionStates = exports.fetchInvoicingConfig = exports.enableInvoicing = exports.acceptInvoicingTerms = exports.toInvoicingQboCustomerMatchingMode = exports.toInvoicingNamedOptionsByKey = exports.getInvoicingQboAccountMappingId = exports.INVOICING_QBO_CUSTOMER_MATCHING_MODES = exports.getInvoicingQboView = exports.invoicingAccountsForQboAccountTypes = exports.startInvoicingQboBackfill = exports.retryInvoicingQboSyncTask = exports.fetchInvoicingQboSyncHealth = exports.fetchInvoicingQboConnection = exports.fetchInvoicingAccountingPickers = exports.isStripeConnectedFromSettings = exports.initialInvoicingBrandingFormLocalData = exports.entityToInvoicingBrandingFormLocalData = exports.INVOICING_BUSINESS_ADDRESS_TYPE = void 0;
|
|
89
89
|
const allowedValue_1 = require("./commonStateTypes/allowedValue");
|
|
90
90
|
Object.defineProperty(exports, "isAllowedValueWithCode", { enumerable: true, get: function () { return allowedValue_1.isAllowedValueWithCode; } });
|
|
91
91
|
Object.defineProperty(exports, "isAllowedValueWithID", { enumerable: true, get: function () { return allowedValue_1.isAllowedValueWithID; } });
|
|
@@ -3162,6 +3162,7 @@ var editInvoicingCatalogItemDetailViewReducer_3 = require("./view/invoicing/edit
|
|
|
3162
3162
|
Object.defineProperty(exports, "fetchInvoicingCatalogItemDetail", { enumerable: true, get: function () { return editInvoicingCatalogItemDetailViewReducer_3.fetchInvoicingCatalogItemDetail; } });
|
|
3163
3163
|
var planSelector_1 = require("./entity/invoicing/plan/planSelector");
|
|
3164
3164
|
Object.defineProperty(exports, "getInvoicingPlanById", { enumerable: true, get: function () { return planSelector_1.getInvoicingPlanById; } });
|
|
3165
|
+
Object.defineProperty(exports, "getLowestInvoicingPlanPricePointAmount", { enumerable: true, get: function () { return planSelector_1.getLowestInvoicingPlanPricePointAmount; } });
|
|
3165
3166
|
var productSelector_1 = require("./entity/invoicing/product/productSelector");
|
|
3166
3167
|
Object.defineProperty(exports, "getInvoicingProductById", { enumerable: true, get: function () { return productSelector_1.getInvoicingProductById; } });
|
|
3167
3168
|
// ── Invoicing: settings ───────────────────────────────────────────────────────
|
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';
|
|
@@ -30,6 +31,15 @@ export interface EditInvoicingCouponDetailView extends SelectorView {
|
|
|
30
31
|
*/
|
|
31
32
|
recordState: FetchStateAndError;
|
|
32
33
|
coupon?: InvoicingCoupon;
|
|
34
|
+
/**
|
|
35
|
+
* The name of the account the coupon's discount line posts to, resolved from
|
|
36
|
+
* the id the coupon stores against the shared QuickBooks account list.
|
|
37
|
+
* Resolved here rather than carried on the entity: the account is a record
|
|
38
|
+
* the accounting lists already own, and the coupon only ever stored a
|
|
39
|
+
* reference to it. Absent while the list is still loading, or when the id
|
|
40
|
+
* names an account this tenant can no longer see.
|
|
41
|
+
*/
|
|
42
|
+
discountsAccountName?: string;
|
|
33
43
|
savedCouponId?: ID;
|
|
34
44
|
}
|
|
35
45
|
export declare const getEditInvoicingCouponDetailViewState: (state: RootState, couponId?: ID) => EditInvoicingCouponDetailView;
|
|
@@ -42,9 +52,31 @@ export interface InvoicingCouponFormView extends EditInvoicingCouponDetailView {
|
|
|
42
52
|
* Folded in here so the screen never reaches for the config view itself.
|
|
43
53
|
*/
|
|
44
54
|
fieldHelp: InvoicingFieldHelpByCode;
|
|
55
|
+
/**
|
|
56
|
+
* False hides the discounts-account picker outright: with no QuickBooks
|
|
57
|
+
* connection there are no accounts to offer, and a coupon saves fine without
|
|
58
|
+
* one.
|
|
59
|
+
*/
|
|
60
|
+
isQboConnected: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* True while the accounting lists are still loading, so the discounts-account
|
|
63
|
+
* picker can show as busy rather than as an empty list.
|
|
64
|
+
*/
|
|
65
|
+
isQboPickerDataLoading: boolean;
|
|
45
66
|
localData: InvoicingCouponFormLocalData;
|
|
46
67
|
mode: 'create' | 'edit';
|
|
47
68
|
periodUnitOptions: InvoicingFormOption[];
|
|
69
|
+
/**
|
|
70
|
+
* The account tree the discounts-account picker renders, and the QuickBooks
|
|
71
|
+
* ids within it the discounts slot accepts. Two values rather than a filtered
|
|
72
|
+
* tree: the picker groups by parent, and dropping a parent that is not itself
|
|
73
|
+
* an income account would take its income children with it.
|
|
74
|
+
*
|
|
75
|
+
* Empty until QBO is connected — the form must not offer a mapping that
|
|
76
|
+
* cannot be pushed.
|
|
77
|
+
*/
|
|
78
|
+
qboAccountHierarchy: NestedAccountHierarchyForReport[];
|
|
79
|
+
qboIncomeAccountIds: string[];
|
|
48
80
|
}
|
|
49
81
|
/**
|
|
50
82
|
* 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,
|
|
@@ -26,6 +28,9 @@ const getEditInvoicingCouponDetailViewState = (state, couponId) => {
|
|
|
26
28
|
fetchState: 'Not-Started',
|
|
27
29
|
})
|
|
28
30
|
: { error: undefined, fetchState: 'Completed' };
|
|
31
|
+
const coupon = couponId != null
|
|
32
|
+
? (0, couponSelector_1.getInvoicingCouponById)(state.invoicingCouponState, couponId)
|
|
33
|
+
: undefined;
|
|
29
34
|
return {
|
|
30
35
|
actionState: couponId != null
|
|
31
36
|
? (state.editInvoicingCouponDetailViewState.actionStateById[couponId] ??
|
|
@@ -39,9 +44,8 @@ const getEditInvoicingCouponDetailViewState = (state, couponId) => {
|
|
|
39
44
|
{ error: undefined, fetchState: configFetchState },
|
|
40
45
|
]),
|
|
41
46
|
recordState,
|
|
42
|
-
coupon
|
|
43
|
-
|
|
44
|
-
: undefined,
|
|
47
|
+
coupon,
|
|
48
|
+
discountsAccountName: (0, invoicingQboViewSelector_1.invoicingQboAccountByQboId)(state, coupon?.discountsAccountId)?.accountName,
|
|
45
49
|
savedCouponId,
|
|
46
50
|
fetchState,
|
|
47
51
|
error,
|
|
@@ -49,6 +53,14 @@ const getEditInvoicingCouponDetailViewState = (state, couponId) => {
|
|
|
49
53
|
};
|
|
50
54
|
};
|
|
51
55
|
exports.getEditInvoicingCouponDetailViewState = getEditInvoicingCouponDetailViewState;
|
|
56
|
+
/**
|
|
57
|
+
* The account types the discounts slot falls back to when the config has no
|
|
58
|
+
* `discounts_account` kind. A QuickBooks discount line posts to an income
|
|
59
|
+
* account; offering every account instead would let a save be rejected.
|
|
60
|
+
*/
|
|
61
|
+
const DEFAULT_DISCOUNTS_ACCOUNT_TYPES = 'Income';
|
|
62
|
+
/** The account-mapping slot a coupon's discounts account shares with settings. */
|
|
63
|
+
const DISCOUNTS_ACCOUNT_MAPPING_SLOT = 'discounts_account';
|
|
52
64
|
/**
|
|
53
65
|
* Bundled view for the discount form screen: the edit view (save state + the
|
|
54
66
|
* coupon to prefill in edit mode), the draft `localData` (seeded from the
|
|
@@ -64,15 +76,64 @@ const getInvoicingCouponFormView = (state, couponId) => {
|
|
|
64
76
|
const localData = couponId != null
|
|
65
77
|
? (editDraftById[couponId] ?? editInvoicingCouponDetailViewState_1.initialInvoicingCouponFormLocalData)
|
|
66
78
|
: newCouponDraft;
|
|
67
|
-
const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, } = (0, invoicingConfigViewSelector_1.getInvoicingConfigView)(state);
|
|
79
|
+
const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, qboAccountMappingKindOptions, } = (0, invoicingConfigViewSelector_1.getInvoicingConfigView)(state);
|
|
80
|
+
const isQboConnected = (0, invoicingQboViewSelector_1.isInvoicingQboConnected)(state);
|
|
81
|
+
const qboView = isQboConnected ? (0, invoicingQboViewSelector_1.getInvoicingQboView)(state) : undefined;
|
|
82
|
+
const isQboPickerDataLoading = (0, invoicingQboViewSelector_1.isInvoicingAccountingPickerDataLoading)(state);
|
|
83
|
+
const settingsFetchState = (0, settingsViewSelector_1.getInvoicingSettingsFetchState)(state).fetchState;
|
|
84
|
+
// Only a create reads the settings, and only until they arrive: an `Error`
|
|
85
|
+
// is a create that starts on no account, not a page that never renders.
|
|
86
|
+
const isCreateSeedPending = couponId == null &&
|
|
87
|
+
(settingsFetchState === 'Not-Started' ||
|
|
88
|
+
settingsFetchState === 'In-Progress');
|
|
89
|
+
// The account types the discounts slot accepts come from the same config the
|
|
90
|
+
// QBO settings page reads, so the two pickers offer the same accounts and
|
|
91
|
+
// widening the slot stays a config change. Matched on the slot rather than
|
|
92
|
+
// the code as written: a config that names the kind `discounts_account_id`
|
|
93
|
+
// is the same slot, and missing it here would quietly fall back to the
|
|
94
|
+
// default and offer a different set than the settings page does.
|
|
95
|
+
const discountsAccountTypes = qboAccountMappingKindOptions.find((kindOption) => (0, invoicingCommonPayload_1.toInvoicingQboAccountMappingSlot)(kindOption.code) ===
|
|
96
|
+
DISCOUNTS_ACCOUNT_MAPPING_SLOT)?.accountTypes ?? DEFAULT_DISCOUNTS_ACCOUNT_TYPES;
|
|
68
97
|
return {
|
|
69
98
|
...base,
|
|
70
99
|
applyOnOptions: discountApplyOnOptions.map(invoicingCommonPayload_1.toInvoicingFormOption),
|
|
71
100
|
discountTypeOptions: discountTypeOptions.map(invoicingCommonPayload_1.toInvoicingFormOption),
|
|
72
101
|
durationTypeOptions: discountDurationTypeOptions.map(invoicingCommonPayload_1.toInvoicingFormOption),
|
|
73
|
-
|
|
102
|
+
isQboConnected,
|
|
103
|
+
isQboPickerDataLoading,
|
|
104
|
+
// Widened past the record and config the detail page waits on: the form
|
|
105
|
+
// also renders from the account list, and seeds a create from the settings
|
|
106
|
+
// discounts account. A form that mounted before those landed would offer an
|
|
107
|
+
// empty picker and save a coupon with no account of its own — not wrong,
|
|
108
|
+
// since blank means "use the settings account", but not what the user was
|
|
109
|
+
// shown either. Widened here rather than on the base view, so the detail
|
|
110
|
+
// page does not start waiting on lists it never renders.
|
|
111
|
+
//
|
|
112
|
+
// Only while they are still coming, though, and the settings only on a
|
|
113
|
+
// create: a coupon saves fine without a discounts account, so neither is
|
|
114
|
+
// allowed to hold the form behind a skeleton once it has failed.
|
|
115
|
+
pageFetchState: (0, reduceFetchState_1.reduceAllFetchState)([
|
|
116
|
+
base.pageFetchState,
|
|
117
|
+
{
|
|
118
|
+
error: undefined,
|
|
119
|
+
fetchState: isQboPickerDataLoading || isCreateSeedPending
|
|
120
|
+
? 'In-Progress'
|
|
121
|
+
: 'Completed',
|
|
122
|
+
},
|
|
123
|
+
]),
|
|
124
|
+
localData: couponId != null
|
|
125
|
+
? localData
|
|
126
|
+
: {
|
|
127
|
+
...localData,
|
|
128
|
+
// Seeded from settings only while the draft has none of its own, so
|
|
129
|
+
// the picker shows the account a create would post against before
|
|
130
|
+
// the user touches it, and stops seeding the moment they pick.
|
|
131
|
+
discountsAccountId: (0, editInvoicingCouponDetailViewState_1.invoicingCouponDiscountsAccountIdForCreate)(localData.discountsAccountId, (0, settingsViewSelector_1.getInvoicingSettings)(state)?.qbo?.account_mappings),
|
|
132
|
+
},
|
|
74
133
|
mode: couponId != null ? 'edit' : 'create',
|
|
75
134
|
periodUnitOptions: billingPeriodUnitOptions.map(invoicingCommonPayload_1.toInvoicingFormOption),
|
|
135
|
+
qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? [],
|
|
136
|
+
qboIncomeAccountIds: (0, invoicingQboViewSelector_1.invoicingAccountsForQboAccountTypes)(qboView?.accounts ?? [], discountsAccountTypes).map((account) => account.id),
|
|
76
137
|
};
|
|
77
138
|
};
|
|
78
139
|
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,20 @@ 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) => {
|
|
65
|
+
const pickedAccountId = discountsAccountId.trim();
|
|
66
|
+
return pickedAccountId !== ''
|
|
67
|
+
? pickedAccountId
|
|
68
|
+
: ((0, invoicingCommonPayload_1.getInvoicingQboAccountMappingId)(accountMappings ?? undefined, 'discounts_account') ?? '');
|
|
69
|
+
};
|
|
70
|
+
exports.invoicingCouponDiscountsAccountIdForCreate = invoicingCouponDiscountsAccountIdForCreate;
|
package/lib/view/invoicing/editInvoicingCouponDetailView/fetchInvoicingCouponDetailPageEpic.d.ts
CHANGED
|
@@ -2,12 +2,16 @@ import { ActionsObservable, StateObservable } from 'redux-observable';
|
|
|
2
2
|
import { Observable } from 'rxjs';
|
|
3
3
|
import { RootState } from '../../../rootStateTypes';
|
|
4
4
|
import { fetchInvoicingConfig } from '../invoicingConfigView/invoicingConfigViewReducer';
|
|
5
|
+
import { fetchInvoicingAccountingPickers, fetchInvoicingQboConnection } from '../invoicingQboView/invoicingQboViewReducer';
|
|
6
|
+
import { fetchInvoicingSettings } from '../settingsView/settingsViewReducer';
|
|
5
7
|
import { fetchInvoicingCouponDetail, fetchInvoicingCouponDetailPage } from './editInvoicingCouponDetailViewReducer';
|
|
6
|
-
export type ActionType = ReturnType<typeof fetchInvoicingCouponDetailPage> | ReturnType<typeof fetchInvoicingCouponDetail> | ReturnType<typeof fetchInvoicingConfig>;
|
|
8
|
+
export type ActionType = ReturnType<typeof fetchInvoicingCouponDetailPage> | ReturnType<typeof fetchInvoicingCouponDetail> | ReturnType<typeof fetchInvoicingConfig> | ReturnType<typeof fetchInvoicingQboConnection> | ReturnType<typeof fetchInvoicingAccountingPickers> | ReturnType<typeof fetchInvoicingSettings>;
|
|
7
9
|
/**
|
|
8
|
-
* Orchestrates the discount
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Orchestrates the discount pages: one action fans out to the coupon record,
|
|
11
|
+
* the invoicing config behind its dropdowns and field help, and the QuickBooks
|
|
12
|
+
* connection, account list and settings the form's Discounts account picker
|
|
13
|
+
* renders and seeds from — each skipped when already cached. The page reads a
|
|
14
|
+
* single reduced fetch state off the view, so the form mounts once with real
|
|
15
|
+
* values.
|
|
12
16
|
*/
|
|
13
17
|
export declare const fetchInvoicingCouponDetailPageEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>) => Observable<ActionType>;
|
package/lib/view/invoicing/editInvoicingCouponDetailView/fetchInvoicingCouponDetailPageEpic.js
CHANGED
|
@@ -4,6 +4,8 @@ exports.fetchInvoicingCouponDetailPageEpic = void 0;
|
|
|
4
4
|
const rxjs_1 = require("rxjs");
|
|
5
5
|
const operators_1 = require("rxjs/operators");
|
|
6
6
|
const invoicingConfigViewReducer_1 = require("../invoicingConfigView/invoicingConfigViewReducer");
|
|
7
|
+
const invoicingQboViewReducer_1 = require("../invoicingQboView/invoicingQboViewReducer");
|
|
8
|
+
const settingsViewReducer_1 = require("../settingsView/settingsViewReducer");
|
|
7
9
|
const editInvoicingCouponDetailViewReducer_1 = require("./editInvoicingCouponDetailViewReducer");
|
|
8
10
|
/**
|
|
9
11
|
* Fetch unless the endpoint is already loaded or in flight — or always when the
|
|
@@ -13,10 +15,12 @@ const shouldFetch = (fetchStateAndError, cacheOverride) => cacheOverride ||
|
|
|
13
15
|
(fetchStateAndError.fetchState !== 'Completed' &&
|
|
14
16
|
fetchStateAndError.fetchState !== 'In-Progress');
|
|
15
17
|
/**
|
|
16
|
-
* Orchestrates the discount
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Orchestrates the discount pages: one action fans out to the coupon record,
|
|
19
|
+
* the invoicing config behind its dropdowns and field help, and the QuickBooks
|
|
20
|
+
* connection, account list and settings the form's Discounts account picker
|
|
21
|
+
* renders and seeds from — each skipped when already cached. The page reads a
|
|
22
|
+
* single reduced fetch state off the view, so the form mounts once with real
|
|
23
|
+
* values.
|
|
20
24
|
*/
|
|
21
25
|
const fetchInvoicingCouponDetailPageEpic = (actions$, state$) => actions$.pipe((0, operators_1.filter)(editInvoicingCouponDetailViewReducer_1.fetchInvoicingCouponDetailPage.match), (0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([action, state]) => {
|
|
22
26
|
const { cacheOverride = false, id } = action.payload;
|
|
@@ -35,6 +39,20 @@ const fetchInvoicingCouponDetailPageEpic = (actions$, state$) => actions$.pipe((
|
|
|
35
39
|
if (shouldFetch({ error: configState.error, fetchState: configState.fetchState }, cacheOverride)) {
|
|
36
40
|
pageActions.push((0, invoicingConfigViewReducer_1.fetchInvoicingConfig)());
|
|
37
41
|
}
|
|
42
|
+
const qboState = state.invoicingQboViewState;
|
|
43
|
+
if (shouldFetch(qboState.connectionFetchState, cacheOverride)) {
|
|
44
|
+
pageActions.push((0, invoicingQboViewReducer_1.fetchInvoicingQboConnection)());
|
|
45
|
+
}
|
|
46
|
+
// The picker epic tops up the shared account list and skips what the
|
|
47
|
+
// store already holds, so this is safe to ask for on every open.
|
|
48
|
+
pageActions.push((0, invoicingQboViewReducer_1.fetchInvoicingAccountingPickers)());
|
|
49
|
+
const settingsState = state.invoicingSettingsViewState;
|
|
50
|
+
if (shouldFetch({ error: settingsState.error, fetchState: settingsState.fetchState }, cacheOverride)) {
|
|
51
|
+
// A create seeds its Discounts account from the settings mapping, so
|
|
52
|
+
// the settings are page data here rather than a settings-screen
|
|
53
|
+
// concern.
|
|
54
|
+
pageActions.push((0, settingsViewReducer_1.fetchInvoicingSettings)());
|
|
55
|
+
}
|
|
38
56
|
return pageActions.length > 0 ? (0, rxjs_1.from)(pageActions) : rxjs_1.EMPTY;
|
|
39
57
|
}));
|
|
40
58
|
exports.fetchInvoicingCouponDetailPageEpic = fetchInvoicingCouponDetailPageEpic;
|
|
@@ -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.46",
|
|
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",
|