@zeniai/client-epic-state 5.2.74 → 5.2.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/lib/commonStateTypes/common.d.ts +3 -0
  2. package/lib/commonStateTypes/common.js +10 -1
  3. package/lib/entity/invoicing/coupon/couponSelector.d.ts +1 -0
  4. package/lib/entity/invoicing/coupon/couponSelector.js +5 -0
  5. package/lib/epic.d.ts +2 -1
  6. package/lib/epic.js +2 -0
  7. package/lib/esm/commonStateTypes/common.js +8 -0
  8. package/lib/esm/entity/invoicing/coupon/couponSelector.js +4 -0
  9. package/lib/esm/epic.js +2 -0
  10. package/lib/esm/index.js +3 -3
  11. package/lib/esm/view/companyTaskManagerView/epics/fetchCompanyTaskManagerViewEpic.js +140 -9
  12. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewPayload.js +55 -0
  13. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.js +35 -1
  14. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +53 -1
  15. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionEstimateEpic.js +49 -0
  16. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +18 -13
  17. package/lib/esm/view/taskManager/taskListView/taskListSelector.js +70 -19
  18. package/lib/index.d.ts +5 -5
  19. package/lib/index.js +18 -15
  20. package/lib/view/companyTaskManagerView/epics/fetchCompanyTaskManagerViewEpic.d.ts +1 -1
  21. package/lib/view/companyTaskManagerView/epics/fetchCompanyTaskManagerViewEpic.js +140 -9
  22. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewPayload.d.ts +46 -1
  23. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewPayload.js +57 -1
  24. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.d.ts +10 -2
  25. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.js +36 -2
  26. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.d.ts +20 -1
  27. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +55 -2
  28. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewState.d.ts +58 -0
  29. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionEstimateEpic.d.ts +7 -0
  30. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionEstimateEpic.js +53 -0
  31. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.d.ts +8 -0
  32. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +21 -14
  33. package/lib/view/taskManager/taskListView/taskListSelector.d.ts +13 -2
  34. package/lib/view/taskManager/taskListView/taskListSelector.js +70 -19
  35. package/package.json +1 -1
@@ -7,19 +7,41 @@ const sortOrderPayload_1 = require("../../../commonPayloadTypes/sortOrderPayload
7
7
  const tagReducer_1 = require("../../../entity/tag/tagReducer");
8
8
  const taskReducer_1 = require("../../../entity/task/taskReducer");
9
9
  const responsePayload_1 = require("../../../responsePayload");
10
+ const zeniDayJS_1 = require("../../../zeniDayJS");
10
11
  const companyTaskManagerViewReducer_1 = require("../companyTaskManagerViewReducer");
11
12
  const DEFAULT_PAGE_SIZE = 100;
12
- const fetchCompanyTaskManagerViewEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(companyTaskManagerViewReducer_1.fetchCompanyTaskManagerView.match), (0, operators_1.switchMap)((action) => {
13
+ // Mutates the accumulator rather than spreading it: spreading rebuilds the whole
14
+ // map on every entry, which is O(n^2) over a tenant's tag list.
15
+ const toTagNameByID = (state) => Object.entries(state.tagState.tagByID).reduce((result, [tagId, tag]) => {
16
+ const name = tag?.name;
17
+ if (name != null && name !== '') {
18
+ result[tagId] = name;
19
+ }
20
+ return result;
21
+ }, {});
22
+ const hasTagCategory = (filters) => (filters?.categories ?? []).some((category) => category.field === 'tag');
23
+ const fetchCompanyTaskManagerViewEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(companyTaskManagerViewReducer_1.fetchCompanyTaskManagerView.match), (0, operators_1.switchMap)((action) => {
13
24
  const { from: fromOffset, searchString, sortKey, sortOrder, filters, } = action.payload;
14
25
  const queryValue = {
15
26
  size: DEFAULT_PAGE_SIZE,
16
27
  from: fromOffset,
17
28
  global_search: searchString ?? '',
29
+ // TasksEsSearch defaults this to true, which AND-injects an
30
+ // `assignees = <caller>` filter. Cockpit's task list is a cross-tenant
31
+ // view grouped by company (and offers an Assignee filter), so it must
32
+ // NOT be scoped to the operator — otherwise every category filter
33
+ // AND's down to their own tasks and returns nothing. The metrics panel
34
+ // deliberately passes `true`; this list deliberately passes `false`.
35
+ is_filter_by_logged_in_user: false,
18
36
  sort_by: toSortKeyPayload(sortKey),
19
37
  sort_order: (0, sortOrderPayload_1.getSortOrder)(sortOrder),
20
38
  filter_join: filters?.categoryCombinationOperator === 'AND' ? 'and' : 'or',
21
39
  ...(filters?.categories != null && filters.categories.length > 0
22
- ? { filter_by: toFilterByPayload(filters) }
40
+ ? {
41
+ // Only walk the tag map when a tag filter is actually in play;
42
+ // otherwise every dispatch pays for a map nothing reads.
43
+ filter_by: toFilterByPayload(filters, hasTagCategory(filters) ? toTagNameByID(state$.value) : {}),
44
+ }
23
45
  : {}),
24
46
  task_group_scopes: filters?.taskGroupScopes != null && filters.taskGroupScopes.length > 0
25
47
  ? filters.taskGroupScopes
@@ -67,14 +89,123 @@ const toSortKeyPayload = (sortKey) => {
67
89
  return sortKey;
68
90
  }
69
91
  };
70
- const toFilterByPayload = (filters) => {
92
+ // Cockpit filter-category keys are UI identifiers; the ES index uses its own
93
+ // field names (see TasksEsSearch.get). Anything not listed here already matches
94
+ // the contract (assignees, priority, status, company_id).
95
+ const ES_FIELD_BY_CATEGORY = {
96
+ creationDate: 'create_time',
97
+ dueDate: 'due_date',
98
+ tag: 'tags',
99
+ };
100
+ // ES rejects `is` / `is_not` on date fields — they only accept `between`, whose
101
+ // values are the [start, end] pair the dateRange picker already emits.
102
+ const ES_DATE_FIELDS = new Set(['create_time', 'due_date']);
103
+ // The cockpit date picker hands over ZeniDate objects; once filters have round-
104
+ // tripped through redux they can arrive as ISO or date-only strings instead.
105
+ // Both have to reach ES as instants: `create_time`/`update_time` carry a real
106
+ // timestamp, so a date-only upper bound lands on midnight and drops everything
107
+ // created later that day — a single-day Created filter would return almost
108
+ // nothing, which is the empty state this epic exists to fix.
109
+ // A real check rather than `value as ZeniDate`: the cast would silently lie
110
+ // about a raw Date, a number, or anything else redux hands back, leaving the
111
+ // runtime guard as the only thing standing between us and a crash.
112
+ const isZeniDateLike = (value) => value != null &&
113
+ typeof value.isValid === 'function' &&
114
+ typeof value.format === 'function';
115
+ const CALENDAR_DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
116
+ const toDateBound = (value, edge) => {
117
+ // A bare `YYYY-MM-DD` carries no instant to misread — use it verbatim. This
118
+ // is the only genuinely timezone-proof shape, which is why it is worth a
119
+ // fast path of its own.
120
+ if (typeof value === 'string' && CALENDAR_DATE_ONLY.test(value)) {
121
+ return edge === 'start'
122
+ ? `${value}T00:00:00.000Z`
123
+ : `${value}T23:59:59.999Z`;
124
+ }
125
+ // Everything else came from the picker, and the picker emits LOCAL midnight:
126
+ // `ZeniDatePicker` hands react-datepicker's local-midnight Date to
127
+ // `zeniDate(date.toISOString())`. So the instant encodes "midnight on the day
128
+ // the user clicked, in the user's zone" — and the day they clicked is
129
+ // recovered by reading it locally, never in UTC.
130
+ //
131
+ // Reading it in UTC subtracts the offset for every UTC+ operator: clicking
132
+ // "Mar 4" in Asia/Kolkata stores 2026-03-03T18:30:00Z, which is the 3rd in
133
+ // UTC. A single-day filter then returns the wrong day's tasks rather than
134
+ // none, and US-based testing never sees it.
135
+ const parsed = typeof value === 'string' ? (0, zeniDayJS_1.date)(value) : value;
136
+ if (!isZeniDateLike(parsed) || !parsed.isValid()) {
137
+ // Drop the row rather than send garbage: ES rejects the whole request on
138
+ // one bad bound, taking the other filters down with it.
139
+ return '';
140
+ }
141
+ const ymd = parsed.format('YYYY-MM-DD');
142
+ return edge === 'start' ? `${ymd}T00:00:00.000Z` : `${ymd}T23:59:59.999Z`;
143
+ };
144
+ const toFilterByPayload = (filters, tagNameByID) => {
71
145
  if (filters.categories == null) {
72
146
  return [];
73
147
  }
74
- return filters.categories.map((category) => ({
75
- field: category.field ?? '',
76
- operator: category.matchingOperator === 'equal' ? 'is' : 'is_not',
77
- values: category.values.map((v) => String(v)),
78
- join: category.valuesCombinationOperator === 'ALL' ? 'and' : 'or',
79
- }));
148
+ return filters.categories.flatMap((category) => {
149
+ const categoryField = category.field ?? '';
150
+ const field = ES_FIELD_BY_CATEGORY[categoryField] ?? categoryField;
151
+ // The dropdown is keyed by tagId, but ES matches `tags` on the tag NAME,
152
+ // so resolve through the tag entity this epic itself populates. Unknown ids
153
+ // fall through unchanged rather than being dropped.
154
+ const isDateField = ES_DATE_FIELDS.has(field);
155
+ // `between` is exactly [from, to]. A half-filled picker (one value) makes ES
156
+ // reject the query outright, and a longer array would quietly bind extras to
157
+ // `end` — both land the user back on an unexplained empty page, which is the
158
+ // failure this epic exists to remove. Drop the row instead.
159
+ if (isDateField && category.values.length !== 2) {
160
+ return [];
161
+ }
162
+ const values = category.values.map((value, index) => {
163
+ if (isDateField) {
164
+ // [from, to] — widen the upper bound to the end of its day.
165
+ return toDateBound(value, index === 0 ? 'start' : 'end');
166
+ }
167
+ const raw = String(value);
168
+ if (field !== 'tags') {
169
+ return raw;
170
+ }
171
+ // An id we cannot resolve would reach ES verbatim and match nothing —
172
+ // another silent empty page. Can happen when a filter is hydrated from the
173
+ // URL before `updateTags` has populated `tagByID`, so keep the value
174
+ // (dropping it would widen the result set) but make it visible in logs.
175
+ const tagName = tagNameByID[raw];
176
+ if (tagName == null) {
177
+ console.debug(`[taskManager] unresolved tagId "${raw}" sent to ES verbatim; expect no matches`);
178
+ }
179
+ return tagName ?? raw;
180
+ });
181
+ // `toDateBound` returns '' for a value it cannot parse. Honour that here:
182
+ // emitting a `between` with an empty bound fails the whole ES query, which
183
+ // takes every OTHER filter in the same request down with it — the silent
184
+ // empty page this epic exists to remove, from one unparseable value that a
185
+ // URL-hydrated filter can supply.
186
+ if (isDateField && values.some((value) => value === '')) {
187
+ return [];
188
+ }
189
+ // ES has no date-exclusion operator: `is`/`is_not` are rejected on date
190
+ // fields, and `between` can only select a range, never exclude one. The
191
+ // operator dropdown still offers "is not" for every category, so an excluded
192
+ // date range would otherwise be sent as a plain `between` — returning exactly
193
+ // the rows the user asked to hide. Drop the row instead: a superset is
194
+ // wrong-but-obvious, an inverted result is wrong-and-invisible.
195
+ if (isDateField && category.matchingOperator !== 'equal') {
196
+ return [];
197
+ }
198
+ return [
199
+ {
200
+ field,
201
+ operator: isDateField
202
+ ? 'between'
203
+ : category.matchingOperator === 'equal'
204
+ ? 'is'
205
+ : 'is_not',
206
+ values,
207
+ join: category.valuesCombinationOperator === 'ALL' ? 'and' : 'or',
208
+ },
209
+ ];
210
+ });
80
211
  };
@@ -1,7 +1,7 @@
1
1
  import { InvoicingSubscriptionPayload } from '../../../entity/invoicing/invoicingSubscription/invoicingSubscriptionPayload';
2
2
  import { ZeniAPIResponse } from '../../../responsePayload';
3
3
  import { AddressUpdatableInfo } from '../../addressView/addressViewState';
4
- import { InvoicingSubscriptionFormLocalData } from './editInvoicingSubscriptionDetailViewState';
4
+ import { InvoicingSubscriptionEstimate, InvoicingSubscriptionFormLocalData } from './editInvoicingSubscriptionDetailViewState';
5
5
  export type SaveInvoicingSubscriptionResponse = ZeniAPIResponse<InvoicingSubscriptionPayload>;
6
6
  /**
7
7
  * Build the create/update subscription request body from the form draft. The
@@ -74,3 +74,48 @@ export type ScheduleChargeBody = {
74
74
  };
75
75
  export type InvoicingSubscriptionActionBody = ApplyCouponBody | RemoveCouponBody | AddChargesBody | PauseSubscriptionBody | CancelSubscriptionBody | ChangePlanBody | ChangeTermEndBody | ScheduleChargeBody | Record<string, never>;
76
76
  export type InvoicingSubscriptionDetailResponse = ZeniAPIResponse<InvoicingSubscriptionPayload>;
77
+ interface InvoicingSubscriptionEstimateLineItemPayload {
78
+ amount: number;
79
+ description: string;
80
+ discount_amount: number;
81
+ quantity: number;
82
+ unit_amount: number;
83
+ date_from?: string;
84
+ date_to?: string;
85
+ tax_amount?: number | null;
86
+ }
87
+ interface InvoicingSubscriptionInvoiceEstimatePayload {
88
+ amount_due: number;
89
+ coupon_discount_total: number;
90
+ credits_applied: number;
91
+ gross_sub_total: number;
92
+ line_items: InvoicingSubscriptionEstimateLineItemPayload[];
93
+ promotional_credits_applied: number;
94
+ sub_total: number;
95
+ tax: number | null;
96
+ total: number;
97
+ due_date?: string;
98
+ invoice_date?: string;
99
+ }
100
+ export interface InvoicingSubscriptionEstimatePayload {
101
+ currency_code: string;
102
+ currency_symbol: string;
103
+ invoice_estimate: InvoicingSubscriptionInvoiceEstimatePayload | null;
104
+ next_invoice_estimate: InvoicingSubscriptionInvoiceEstimatePayload | null;
105
+ proration_estimate: {
106
+ document_type: string;
107
+ invoice_estimate: InvoicingSubscriptionInvoiceEstimatePayload | null;
108
+ net: number;
109
+ } | null;
110
+ reason: string;
111
+ subscription_estimate: {
112
+ becomes_non_renewing_after_this_term?: boolean;
113
+ current_term_end?: string;
114
+ current_term_start?: string;
115
+ next_billing_at?: string | null;
116
+ } | null;
117
+ will_be_invoiced: boolean;
118
+ }
119
+ export type InvoicingSubscriptionEstimateResponse = ZeniAPIResponse<InvoicingSubscriptionEstimatePayload>;
120
+ export declare const mapInvoicingSubscriptionEstimatePayloadToState: (payload: InvoicingSubscriptionEstimatePayload) => InvoicingSubscriptionEstimate;
121
+ export {};
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.localDataToSaveInvoicingSubscriptionPayload = void 0;
3
+ exports.mapInvoicingSubscriptionEstimatePayloadToState = exports.localDataToSaveInvoicingSubscriptionPayload = void 0;
4
+ const amount_1 = require("../../../commonStateTypes/amount");
5
+ const zeniDayJS_1 = require("../../../zeniDayJS");
4
6
  const addressViewPayload_1 = require("../../addressView/addressViewPayload");
5
7
  const invoicingSubscriptionFormConfig_1 = require("./invoicingSubscriptionFormConfig");
6
8
  /**
@@ -60,3 +62,57 @@ const localDataToSaveInvoicingSubscriptionPayload = (localData, shippingAddress)
60
62
  };
61
63
  };
62
64
  exports.localDataToSaveInvoicingSubscriptionPayload = localDataToSaveInvoicingSubscriptionPayload;
65
+ const toEstimateDate = (value) => value != null && value !== '' ? (0, zeniDayJS_1.date)(value) : undefined;
66
+ const mapInvoiceEstimate = (payload, money) => payload == null
67
+ ? undefined
68
+ : {
69
+ amountDue: money(payload.amount_due),
70
+ couponDiscountTotal: money(payload.coupon_discount_total),
71
+ creditsApplied: money(payload.credits_applied),
72
+ dueDate: toEstimateDate(payload.due_date),
73
+ grossSubTotal: money(payload.gross_sub_total),
74
+ invoiceDate: toEstimateDate(payload.invoice_date),
75
+ lineItems: payload.line_items.map((lineItem) => ({
76
+ amount: money(lineItem.amount),
77
+ dateFrom: toEstimateDate(lineItem.date_from),
78
+ dateTo: toEstimateDate(lineItem.date_to),
79
+ description: lineItem.description,
80
+ discountAmount: money(lineItem.discount_amount),
81
+ quantity: lineItem.quantity,
82
+ taxAmount: lineItem.tax_amount != null
83
+ ? money(lineItem.tax_amount)
84
+ : undefined,
85
+ unitAmount: money(lineItem.unit_amount),
86
+ })),
87
+ promotionalCreditsApplied: money(payload.promotional_credits_applied),
88
+ subTotal: money(payload.sub_total),
89
+ tax: payload.tax != null ? money(payload.tax) : undefined,
90
+ total: money(payload.total),
91
+ };
92
+ const mapInvoicingSubscriptionEstimatePayloadToState = (payload) => {
93
+ const money = (amount) => (0, amount_1.toAmount)(amount, payload.currency_code, payload.currency_symbol);
94
+ const term = payload.subscription_estimate;
95
+ const proration = payload.proration_estimate;
96
+ return {
97
+ invoice: mapInvoiceEstimate(payload.invoice_estimate, money),
98
+ nextInvoice: mapInvoiceEstimate(payload.next_invoice_estimate, money),
99
+ proration: proration != null
100
+ ? {
101
+ documentType: proration.document_type,
102
+ invoice: mapInvoiceEstimate(proration.invoice_estimate, money),
103
+ net: money(proration.net),
104
+ }
105
+ : undefined,
106
+ reason: payload.reason,
107
+ term: term != null
108
+ ? {
109
+ becomesNonRenewing: term.becomes_non_renewing_after_this_term === true,
110
+ currentTermEnd: toEstimateDate(term.current_term_end),
111
+ currentTermStart: toEstimateDate(term.current_term_start),
112
+ nextBillingAt: toEstimateDate(term.next_billing_at),
113
+ }
114
+ : undefined,
115
+ willBeInvoiced: payload.will_be_invoiced,
116
+ };
117
+ };
118
+ exports.mapInvoicingSubscriptionEstimatePayloadToState = mapInvoicingSubscriptionEstimatePayloadToState;
@@ -1,7 +1,7 @@
1
1
  import { ID } from '../../../commonStateTypes/common';
2
2
  import { ZeniAPIStatus } from '../../../responsePayload';
3
3
  import { InvoicingSubscriptionActionBody } from './editInvoicingSubscriptionDetailViewPayload';
4
- import { EditInvoicingSubscriptionDetailViewState, InvoicingSubscriptionFormLocalData } from './editInvoicingSubscriptionDetailViewState';
4
+ import { EditInvoicingSubscriptionDetailViewState, InvoicingSubscriptionEstimate, InvoicingSubscriptionFormLocalData } from './editInvoicingSubscriptionDetailViewState';
5
5
  /** Push the full form draft into CES (edit tree when `id` is present). */
6
6
  export interface UpdateInvoicingSubscriptionFormDraftPayload {
7
7
  localData: InvoicingSubscriptionFormLocalData;
@@ -59,6 +59,14 @@ export declare const runInvoicingSubscriptionAction: import("@reduxjs/toolkit").
59
59
  id?: ID;
60
60
  } | undefined, "editInvoicingSubscriptionDetailView/resetEditInvoicingSubscriptionDetailView">, initializeInvoicingSubscriptionAddress: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[payload: InitializeInvoicingSubscriptionAddressPayload], InitializeInvoicingSubscriptionAddressPayload, "editInvoicingSubscriptionDetailView/initializeInvoicingSubscriptionAddress", never, never>, claimInvoicingSubscriptionShippingAddress: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
61
61
  id: ID;
62
- }, "editInvoicingSubscriptionDetailView/claimInvoicingSubscriptionShippingAddress">, syncInvoicingSubscriptionCoupons: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[payload: SyncInvoicingSubscriptionCouponsPayload], SyncInvoicingSubscriptionCouponsPayload, "editInvoicingSubscriptionDetailView/syncInvoicingSubscriptionCoupons", never, never>;
62
+ }, "editInvoicingSubscriptionDetailView/claimInvoicingSubscriptionShippingAddress">, syncInvoicingSubscriptionCoupons: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[payload: SyncInvoicingSubscriptionCouponsPayload], SyncInvoicingSubscriptionCouponsPayload, "editInvoicingSubscriptionDetailView/syncInvoicingSubscriptionCoupons", never, never>, fetchInvoicingSubscriptionEstimate: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
63
+ id?: ID;
64
+ }, "editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionEstimate">, updateInvoicingSubscriptionEstimate: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
65
+ estimate: InvoicingSubscriptionEstimate;
66
+ id?: ID;
67
+ }, "editInvoicingSubscriptionDetailView/updateInvoicingSubscriptionEstimate">, updateInvoicingSubscriptionEstimateFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
68
+ status: ZeniAPIStatus;
69
+ id?: ID;
70
+ }, "editInvoicingSubscriptionDetailView/updateInvoicingSubscriptionEstimateFailure">, clearInvoicingSubscriptionEstimate: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"editInvoicingSubscriptionDetailView/clearInvoicingSubscriptionEstimate">;
63
71
  declare const _default: import("redux").Reducer<EditInvoicingSubscriptionDetailViewState>;
64
72
  export default _default;
@@ -1,15 +1,19 @@
1
1
  "use strict";
2
2
  var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.syncInvoicingSubscriptionCoupons = exports.claimInvoicingSubscriptionShippingAddress = exports.initializeInvoicingSubscriptionAddress = exports.resetEditInvoicingSubscriptionDetailView = exports.submitInvoicingSubscriptionFormFailure = exports.submitInvoicingSubscriptionFormSuccess = exports.submitInvoicingSubscriptionForm = exports.updateInvoicingSubscriptionFormDraft = exports.updateInvoicingSubscriptionDetailFailure = exports.updateInvoicingSubscriptionDetail = exports.seedInvoicingSubscriptionFormDraft = exports.fetchInvoicingSubscriptionFormPage = exports.fetchInvoicingSubscriptionDetailPage = exports.fetchInvoicingSubscriptionDetail = exports.runInvoicingSubscriptionActionSuccess = exports.runInvoicingSubscriptionActionFailure = exports.runInvoicingSubscriptionAction = exports.initialState = void 0;
4
+ exports.clearInvoicingSubscriptionEstimate = exports.updateInvoicingSubscriptionEstimateFailure = exports.updateInvoicingSubscriptionEstimate = exports.fetchInvoicingSubscriptionEstimate = exports.syncInvoicingSubscriptionCoupons = exports.claimInvoicingSubscriptionShippingAddress = exports.initializeInvoicingSubscriptionAddress = exports.resetEditInvoicingSubscriptionDetailView = exports.submitInvoicingSubscriptionFormFailure = exports.submitInvoicingSubscriptionFormSuccess = exports.submitInvoicingSubscriptionForm = exports.updateInvoicingSubscriptionFormDraft = exports.updateInvoicingSubscriptionDetailFailure = exports.updateInvoicingSubscriptionDetail = exports.seedInvoicingSubscriptionFormDraft = exports.fetchInvoicingSubscriptionFormPage = exports.fetchInvoicingSubscriptionDetailPage = exports.fetchInvoicingSubscriptionDetail = exports.runInvoicingSubscriptionActionSuccess = exports.runInvoicingSubscriptionActionFailure = exports.runInvoicingSubscriptionAction = exports.initialState = void 0;
5
5
  const toolkit_1 = require("@reduxjs/toolkit");
6
6
  const rootActions_1 = require("../../../rootActions");
7
7
  const invoicingFormDraft_1 = require("../invoicingFormDraft");
8
8
  const invoicingSubscriptionFormConfig_1 = require("./invoicingSubscriptionFormConfig");
9
+ const initialEstimateState = {
10
+ fetchState: { fetchState: 'Not-Started', error: undefined },
11
+ };
9
12
  exports.initialState = {
10
13
  fetchState: 'Not-Started',
11
14
  error: undefined,
12
15
  editDraftById: {},
16
+ estimateState: initialEstimateState,
13
17
  actionStateById: {},
14
18
  recordStateById: {},
15
19
  shippingAddressOwnerId: undefined,
@@ -23,6 +27,36 @@ const editInvoicingSubscriptionDetailView = (0, toolkit_1.createSlice)({
23
27
  name: 'editInvoicingSubscriptionDetailView',
24
28
  initialState: exports.initialState,
25
29
  reducers: {
30
+ // What saving the draft would bill (no id: create); see the estimate epic.
31
+ fetchInvoicingSubscriptionEstimate(draft, action) {
32
+ draft.estimateState = {
33
+ fetchState: { fetchState: 'In-Progress', error: undefined },
34
+ subscriptionId: action.payload.id,
35
+ };
36
+ },
37
+ // Dropped when the dialog has since moved to another subscription.
38
+ updateInvoicingSubscriptionEstimate(draft, action) {
39
+ if (draft.estimateState.subscriptionId !== action.payload.id) {
40
+ return;
41
+ }
42
+ draft.estimateState.estimate = action.payload.estimate;
43
+ draft.estimateState.fetchState = {
44
+ fetchState: 'Completed',
45
+ error: undefined,
46
+ };
47
+ },
48
+ updateInvoicingSubscriptionEstimateFailure(draft, action) {
49
+ if (draft.estimateState.subscriptionId !== action.payload.id) {
50
+ return;
51
+ }
52
+ draft.estimateState.fetchState = {
53
+ fetchState: 'Error',
54
+ error: action.payload.status,
55
+ };
56
+ },
57
+ clearInvoicingSubscriptionEstimate(draft) {
58
+ draft.estimateState = initialEstimateState;
59
+ },
26
60
  runInvoicingSubscriptionAction(draft, action) {
27
61
  draft.actionStateById[action.payload.id] = {
28
62
  fetchState: 'In-Progress',
@@ -162,5 +196,5 @@ const editInvoicingSubscriptionDetailView = (0, toolkit_1.createSlice)({
162
196
  });
163
197
  },
164
198
  });
165
- _a = editInvoicingSubscriptionDetailView.actions, exports.runInvoicingSubscriptionAction = _a.runInvoicingSubscriptionAction, exports.runInvoicingSubscriptionActionFailure = _a.runInvoicingSubscriptionActionFailure, exports.runInvoicingSubscriptionActionSuccess = _a.runInvoicingSubscriptionActionSuccess, exports.fetchInvoicingSubscriptionDetail = _a.fetchInvoicingSubscriptionDetail, exports.fetchInvoicingSubscriptionDetailPage = _a.fetchInvoicingSubscriptionDetailPage, exports.fetchInvoicingSubscriptionFormPage = _a.fetchInvoicingSubscriptionFormPage, exports.seedInvoicingSubscriptionFormDraft = _a.seedInvoicingSubscriptionFormDraft, exports.updateInvoicingSubscriptionDetail = _a.updateInvoicingSubscriptionDetail, exports.updateInvoicingSubscriptionDetailFailure = _a.updateInvoicingSubscriptionDetailFailure, exports.updateInvoicingSubscriptionFormDraft = _a.updateInvoicingSubscriptionFormDraft, exports.submitInvoicingSubscriptionForm = _a.submitInvoicingSubscriptionForm, exports.submitInvoicingSubscriptionFormSuccess = _a.submitInvoicingSubscriptionFormSuccess, exports.submitInvoicingSubscriptionFormFailure = _a.submitInvoicingSubscriptionFormFailure, exports.resetEditInvoicingSubscriptionDetailView = _a.resetEditInvoicingSubscriptionDetailView, exports.initializeInvoicingSubscriptionAddress = _a.initializeInvoicingSubscriptionAddress, exports.claimInvoicingSubscriptionShippingAddress = _a.claimInvoicingSubscriptionShippingAddress, exports.syncInvoicingSubscriptionCoupons = _a.syncInvoicingSubscriptionCoupons;
199
+ _a = editInvoicingSubscriptionDetailView.actions, exports.runInvoicingSubscriptionAction = _a.runInvoicingSubscriptionAction, exports.runInvoicingSubscriptionActionFailure = _a.runInvoicingSubscriptionActionFailure, exports.runInvoicingSubscriptionActionSuccess = _a.runInvoicingSubscriptionActionSuccess, exports.fetchInvoicingSubscriptionDetail = _a.fetchInvoicingSubscriptionDetail, exports.fetchInvoicingSubscriptionDetailPage = _a.fetchInvoicingSubscriptionDetailPage, exports.fetchInvoicingSubscriptionFormPage = _a.fetchInvoicingSubscriptionFormPage, exports.seedInvoicingSubscriptionFormDraft = _a.seedInvoicingSubscriptionFormDraft, exports.updateInvoicingSubscriptionDetail = _a.updateInvoicingSubscriptionDetail, exports.updateInvoicingSubscriptionDetailFailure = _a.updateInvoicingSubscriptionDetailFailure, exports.updateInvoicingSubscriptionFormDraft = _a.updateInvoicingSubscriptionFormDraft, exports.submitInvoicingSubscriptionForm = _a.submitInvoicingSubscriptionForm, exports.submitInvoicingSubscriptionFormSuccess = _a.submitInvoicingSubscriptionFormSuccess, exports.submitInvoicingSubscriptionFormFailure = _a.submitInvoicingSubscriptionFormFailure, exports.resetEditInvoicingSubscriptionDetailView = _a.resetEditInvoicingSubscriptionDetailView, exports.initializeInvoicingSubscriptionAddress = _a.initializeInvoicingSubscriptionAddress, exports.claimInvoicingSubscriptionShippingAddress = _a.claimInvoicingSubscriptionShippingAddress, exports.syncInvoicingSubscriptionCoupons = _a.syncInvoicingSubscriptionCoupons, exports.fetchInvoicingSubscriptionEstimate = _a.fetchInvoicingSubscriptionEstimate, exports.updateInvoicingSubscriptionEstimate = _a.updateInvoicingSubscriptionEstimate, exports.updateInvoicingSubscriptionEstimateFailure = _a.updateInvoicingSubscriptionEstimateFailure, exports.clearInvoicingSubscriptionEstimate = _a.clearInvoicingSubscriptionEstimate;
166
200
  exports.default = editInvoicingSubscriptionDetailView.reducer;
@@ -5,7 +5,8 @@ import { InvoicingCatalogPricePoint } from '../../../entity/invoicing/invoicingC
5
5
  import { InvoicingFieldHelpByCode, InvoicingNamedOption, InvoicingSubscriptionLifecycleActionOption } from '../../../entity/invoicing/invoicingCommonPayload';
6
6
  import { InvoicingSubscription } from '../../../entity/invoicing/invoicingSubscription/invoicingSubscriptionState';
7
7
  import { RootState } from '../../../rootStateTypes';
8
- import { InvoicingSubscriptionFormLocalData } from './editInvoicingSubscriptionDetailViewState';
8
+ import { ZeniDate } from '../../../zeniDayJS';
9
+ import { InvoicingSubscriptionEstimateState, InvoicingSubscriptionFormLocalData, InvoicingSubscriptionInvoiceEstimate } from './editInvoicingSubscriptionDetailViewState';
9
10
  /**
10
11
  * Bundled view for the subscription create/edit form's save concern: the save
11
12
  * fetch state/error, the id of the subscription last saved, and — when an
@@ -29,6 +30,22 @@ export interface EditInvoicingSubscriptionDetailView extends SelectorView {
29
30
  * `{label, value}` records, so this stays free of any web-components
30
31
  * dependency while leaving the screen nothing to derive.
31
32
  */
33
+ export type InvoicingSubscriptionEstimateKind = 'invoice_now' | 'later_invoice' | 'proration_credit' | 'proration_invoice' | 'reason_only';
34
+ export interface InvoicingSubscriptionEstimateView {
35
+ /** A later invoice quotes today's credit balances, which can change by then. */
36
+ creditsMayChange: boolean;
37
+ fetchState: FetchStateAndError;
38
+ nonRenewing: boolean;
39
+ /** The one invoice to show, whichever estimate it came from. */
40
+ invoice?: InvoicingSubscriptionInvoiceEstimate;
41
+ kind?: InvoicingSubscriptionEstimateKind;
42
+ nextBillingAt?: ZeniDate;
43
+ reason?: string;
44
+ storeCredit?: Amount;
45
+ termEnd?: ZeniDate;
46
+ termStart?: ZeniDate;
47
+ }
48
+ export declare const toInvoicingSubscriptionEstimateView: ({ estimate, fetchState, subscriptionId }: InvoicingSubscriptionEstimateState, invoicingSubscriptionID?: ID) => InvoicingSubscriptionEstimateView;
32
49
  export interface InvoicingSubscriptionFormView extends SelectorView {
33
50
  /** Lifecycle action in flight, folded in from the record view. */
34
51
  actionState: FetchStateAndError;
@@ -43,6 +60,8 @@ export interface InvoicingSubscriptionFormView extends SelectorView {
43
60
  charges: InvoicingSubscriptionOption[];
44
61
  coupons: InvoicingSubscriptionOption[];
45
62
  customers: InvoicingSubscriptionOption[];
63
+ /** The review dialog's cost estimate for saving this draft. */
64
+ estimate: InvoicingSubscriptionEstimateView;
46
65
  fieldHelp: InvoicingFieldHelpByCode;
47
66
  /**
48
67
  * True while the subscription being edited is still on its way to the form.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getInvoicingSubscriptionFormView = exports.isInvoicingAddonPeriodCompatible = exports.getInvoicingItemsAllowedForPlan = exports.getEditInvoicingSubscriptionDetailViewState = void 0;
3
+ exports.getInvoicingSubscriptionFormView = exports.isInvoicingAddonPeriodCompatible = exports.getInvoicingItemsAllowedForPlan = exports.getEditInvoicingSubscriptionDetailViewState = exports.toInvoicingSubscriptionEstimateView = void 0;
4
4
  const reduceFetchState_1 = require("../../../commonStateTypes/reduceFetchState");
5
5
  const addressSelector_1 = require("../../../entity/address/addressSelector");
6
6
  const invoicingSubscriptionSelector_1 = require("../../../entity/invoicing/invoicingSubscription/invoicingSubscriptionSelector");
@@ -12,6 +12,58 @@ const invoicingCustomerListViewSelector_1 = require("../invoicingCustomerListVie
12
12
  const invoicingFormDraft_1 = require("../invoicingFormDraft");
13
13
  const invoicingSubscriptionFormConfig_1 = require("./invoicingSubscriptionFormConfig");
14
14
  const invoicingSubscriptionFormConfig_2 = require("./invoicingSubscriptionFormConfig");
15
+ const notStartedEstimateView = {
16
+ creditsMayChange: false,
17
+ fetchState: { fetchState: 'Not-Started', error: undefined },
18
+ nonRenewing: false,
19
+ };
20
+ const toInvoicingSubscriptionEstimateView = ({ estimate, fetchState, subscriptionId }, invoicingSubscriptionID) => {
21
+ // A slot asked for another subscription is not this form's estimate.
22
+ if (subscriptionId !== invoicingSubscriptionID) {
23
+ return notStartedEstimateView;
24
+ }
25
+ if (estimate == null) {
26
+ return { ...notStartedEstimateView, fetchState };
27
+ }
28
+ const { invoice, nextInvoice, proration, reason, term } = estimate;
29
+ const base = {
30
+ fetchState,
31
+ nonRenewing: term?.becomesNonRenewing === true,
32
+ nextBillingAt: term?.nextBillingAt,
33
+ reason: reason !== '' ? reason : undefined,
34
+ termEnd: term?.currentTermEnd,
35
+ termStart: term?.currentTermStart,
36
+ };
37
+ if (proration?.documentType === 'invoice' && proration.invoice != null) {
38
+ return {
39
+ ...base,
40
+ creditsMayChange: false,
41
+ invoice: proration.invoice,
42
+ kind: 'proration_invoice',
43
+ };
44
+ }
45
+ if (proration?.documentType === 'credit_note') {
46
+ return {
47
+ ...base,
48
+ creditsMayChange: false,
49
+ kind: 'proration_credit',
50
+ storeCredit: { ...proration.net, amount: Math.abs(proration.net.amount) },
51
+ };
52
+ }
53
+ if (estimate.willBeInvoiced && invoice != null) {
54
+ return { ...base, creditsMayChange: false, invoice, kind: 'invoice_now' };
55
+ }
56
+ if (nextInvoice != null) {
57
+ return {
58
+ ...base,
59
+ creditsMayChange: true,
60
+ invoice: nextInvoice,
61
+ kind: 'later_invoice',
62
+ };
63
+ }
64
+ return { ...base, creditsMayChange: false, kind: 'reason_only' };
65
+ };
66
+ exports.toInvoicingSubscriptionEstimateView = toInvoicingSubscriptionEstimateView;
15
67
  const getEditInvoicingSubscriptionDetailViewState = (state, invoicingSubscriptionID) => {
16
68
  const { error, fetchState, savedSubscriptionId } = state.editInvoicingSubscriptionDetailViewState;
17
69
  const { subscriptionCommercialEditAllowedFromStatuses, subscriptionLifecycleActionOptions, subscriptionMetadataEditAllowedFromStatuses, } = (0, invoicingConfigViewSelector_1.getInvoicingConfigView)(state);
@@ -176,7 +228,7 @@ const toInvoicingCustomerOption = (customer) => ({
176
228
  value: customer.id,
177
229
  });
178
230
  const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID) => {
179
- const { editDraftById, error, fetchState, newSubscriptionDraft, savedSubscriptionId, } = state.editInvoicingSubscriptionDetailViewState;
231
+ const { editDraftById, error, estimateState, fetchState, newSubscriptionDraft, savedSubscriptionId, } = state.editInvoicingSubscriptionDetailViewState;
180
232
  const mode = invoicingSubscriptionID != null ? 'edit' : 'create';
181
233
  const subscription = invoicingSubscriptionID != null
182
234
  ? (0, invoicingSubscriptionSelector_1.getInvoicingSubscriptionById)(state.invoicingSubscriptionState, invoicingSubscriptionID)
@@ -230,6 +282,7 @@ const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID) => {
230
282
  subscriptionMetadataEditAllowedFromStatuses: configState.subscriptionMetadataEditAllowedFromStatuses,
231
283
  recordState,
232
284
  isFormLoading: (0, invoicingFormDraft_1.isInvoicingFormDraftPending)(editDraftById, invoicingSubscriptionID),
285
+ estimate: (0, exports.toInvoicingSubscriptionEstimateView)(estimateState, invoicingSubscriptionID),
233
286
  fieldHelp: (0, invoicingConfigViewSelector_1.invoicingFieldHelpForGroup)(state, 'subscription'),
234
287
  addons: catalogItemOptionsOfType('addon'),
235
288
  charges: catalogItemOptionsOfType('charge'),
@@ -1,5 +1,6 @@
1
1
  import { Amount } from '../../../commonStateTypes/amount';
2
2
  import { FetchStateAndError, FetchedState, ID } from '../../../commonStateTypes/common';
3
+ import { ZeniDate } from '../../../zeniDayJS';
3
4
  import { InvoicingFormDraftById } from '../invoicingFormDraft';
4
5
  /**
5
6
  * Draft form state for the create/edit subscription form. Held in CES so the
@@ -39,10 +40,67 @@ export interface InvoicingSubscriptionFormLocalData {
39
40
  /** Unset until the user enters one, so an explicit 0 stays distinguishable. */
40
41
  setupFee?: Amount;
41
42
  }
43
+ export interface InvoicingSubscriptionEstimateLineItem {
44
+ amount: Amount;
45
+ description: string;
46
+ discountAmount: Amount;
47
+ quantity: number;
48
+ unitAmount: Amount;
49
+ dateFrom?: ZeniDate;
50
+ dateTo?: ZeniDate;
51
+ taxAmount?: Amount;
52
+ }
53
+ /** An invoice the save would raise now, or the next one it would raise later. */
54
+ export interface InvoicingSubscriptionInvoiceEstimate {
55
+ amountDue: Amount;
56
+ couponDiscountTotal: Amount;
57
+ /** Refundable credits plus excess payments; promotional credits are separate. */
58
+ creditsApplied: Amount;
59
+ grossSubTotal: Amount;
60
+ lineItems: InvoicingSubscriptionEstimateLineItem[];
61
+ promotionalCreditsApplied: Amount;
62
+ /** After coupons and promotional credits. */
63
+ subTotal: Amount;
64
+ total: Amount;
65
+ dueDate?: ZeniDate;
66
+ invoiceDate?: ZeniDate;
67
+ /** Absent when the tax quote failed on a later invoice; Save still succeeds. */
68
+ tax?: Amount;
69
+ }
70
+ export interface InvoicingSubscriptionProrationEstimate {
71
+ /** `invoice`, `credit_note`, or `none` when the change nets to zero. */
72
+ documentType: string;
73
+ net: Amount;
74
+ invoice?: InvoicingSubscriptionInvoiceEstimate;
75
+ }
76
+ export interface InvoicingSubscriptionTermEstimate {
77
+ becomesNonRenewing: boolean;
78
+ currentTermEnd?: ZeniDate;
79
+ currentTermStart?: ZeniDate;
80
+ nextBillingAt?: ZeniDate;
81
+ }
82
+ /** What a save would bill, from `POST /subscriptions[/{id}]/estimate`. */
83
+ export interface InvoicingSubscriptionEstimate {
84
+ /** The service's sentence for why nothing is invoiced now; empty otherwise. */
85
+ reason: string;
86
+ willBeInvoiced: boolean;
87
+ invoice?: InvoicingSubscriptionInvoiceEstimate;
88
+ nextInvoice?: InvoicingSubscriptionInvoiceEstimate;
89
+ proration?: InvoicingSubscriptionProrationEstimate;
90
+ term?: InvoicingSubscriptionTermEstimate;
91
+ }
92
+ export interface InvoicingSubscriptionEstimateState {
93
+ fetchState: FetchStateAndError;
94
+ estimate?: InvoicingSubscriptionEstimate;
95
+ /** The subscription the estimate was asked for; absent for a new one. */
96
+ subscriptionId?: ID;
97
+ }
42
98
  export interface EditInvoicingSubscriptionDetailViewState extends FetchedState {
43
99
  /** Lifecycle action fetch state per subscription id. */
44
100
  actionStateById: Record<ID, FetchStateAndError>;
45
101
  editDraftById: InvoicingFormDraftById<InvoicingSubscriptionFormLocalData>;
102
+ /** The cost estimate shown before a save; nothing is saved by asking. */
103
+ estimateState: InvoicingSubscriptionEstimateState;
46
104
  newSubscriptionDraft: InvoicingSubscriptionFormLocalData;
47
105
  /** Detail-load state per subscription id. */
48
106
  recordStateById: Record<ID, FetchStateAndError>;
@@ -0,0 +1,7 @@
1
+ import { ActionsObservable, StateObservable } from 'redux-observable';
2
+ import { Observable } from 'rxjs';
3
+ import { RootState } from '../../../rootStateTypes';
4
+ import { ZeniAPI } from '../../../zeniAPI';
5
+ import { fetchInvoicingSubscriptionEstimate, updateInvoicingSubscriptionEstimate, updateInvoicingSubscriptionEstimateFailure } from './editInvoicingSubscriptionDetailViewReducer';
6
+ export type ActionType = ReturnType<typeof fetchInvoicingSubscriptionEstimate> | ReturnType<typeof updateInvoicingSubscriptionEstimate> | ReturnType<typeof updateInvoicingSubscriptionEstimateFailure>;
7
+ export declare const fetchInvoicingSubscriptionEstimateEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;