@meetelise/chat 1.55.0 → 1.55.1
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/dist/src/WebComponent/FeeCalculator/components/charge-inputs/charge-inputs.d.ts +3 -0
- package/dist/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout.d.ts +4 -1
- package/dist/src/WebComponent/FeeCalculator/fee-calculator.d.ts +3 -0
- package/dist/src/WebComponent/FeeCalculator/model/marketable-fee.d.ts +2 -0
- package/dist/src/WebComponent/FeeCalculator/utils/pet-limit-utils.d.ts +10 -0
- package/dist/src/services/fees/calculateQuote.d.ts +14 -1
- package/package.json +1 -1
- package/public/dist/index.js +245 -217
- package/src/WebComponent/FeeCalculator/components/charge-inputs/charge-inputs.ts +44 -10
- package/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout-styles.ts +7 -0
- package/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout.ts +32 -1
- package/src/WebComponent/FeeCalculator/fee-calculator.ts +26 -11
- package/src/WebComponent/FeeCalculator/model/marketable-fee.ts +3 -0
- package/src/WebComponent/FeeCalculator/utils/pet-limit-utils.ts +27 -0
- package/src/feeCalculatorPetLimit.test.ts +346 -0
- package/src/services/fees/calculateQuote.ts +38 -3
|
@@ -3,6 +3,13 @@ import { customElement, property } from "lit/decorators.js";
|
|
|
3
3
|
import { ChargeInputs } from "../../../../services/fees/calculateQuote";
|
|
4
4
|
import { RentableItemSummary } from "../../model/rentable-item-summary";
|
|
5
5
|
import { DesiredRentableItem } from "../../model/desired-rentable-item";
|
|
6
|
+
import { formatPetLimit } from "../../utils/pet-limit-utils";
|
|
7
|
+
|
|
8
|
+
const PET_FIELDS: (keyof ChargeInputs)[] = [
|
|
9
|
+
"num_cats",
|
|
10
|
+
"num_dogs",
|
|
11
|
+
"num_other_pets",
|
|
12
|
+
];
|
|
6
13
|
|
|
7
14
|
@customElement("charge-inputs")
|
|
8
15
|
export class ChargeInputsComponent extends LitElement {
|
|
@@ -96,6 +103,12 @@ export class ChargeInputsComponent extends LitElement {
|
|
|
96
103
|
color: #1f2937;
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
.charge-input-note {
|
|
107
|
+
font-size: 12px;
|
|
108
|
+
color: #6b7280;
|
|
109
|
+
margin-top: 8px;
|
|
110
|
+
}
|
|
111
|
+
|
|
99
112
|
.rentable-item-row {
|
|
100
113
|
display: flex;
|
|
101
114
|
justify-content: space-between;
|
|
@@ -154,6 +167,9 @@ export class ChargeInputsComponent extends LitElement {
|
|
|
154
167
|
@property({ type: Boolean })
|
|
155
168
|
disabled = false;
|
|
156
169
|
|
|
170
|
+
@property({ type: Number })
|
|
171
|
+
maxPets: number | null = null;
|
|
172
|
+
|
|
157
173
|
@property({ type: Array })
|
|
158
174
|
rentableItems: RentableItemSummary[] = [];
|
|
159
175
|
|
|
@@ -166,15 +182,29 @@ export class ChargeInputsComponent extends LitElement {
|
|
|
166
182
|
@property()
|
|
167
183
|
onRentableItemRemove: ((item: DesiredRentableItem) => void) | null = null;
|
|
168
184
|
|
|
185
|
+
private get totalPets(): number {
|
|
186
|
+
return (
|
|
187
|
+
(this.chargeInputs.num_cats ?? 0) +
|
|
188
|
+
(this.chargeInputs.num_dogs ?? 0) +
|
|
189
|
+
(this.chargeInputs.num_other_pets ?? 0)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private isAtPetLimit(field: keyof ChargeInputs): boolean {
|
|
194
|
+
return (
|
|
195
|
+
this.maxPets !== null &&
|
|
196
|
+
PET_FIELDS.includes(field) &&
|
|
197
|
+
this.totalPets >= this.maxPets
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
169
201
|
private handleIncrement(field: keyof ChargeInputs): void {
|
|
202
|
+
if (this.isAtPetLimit(field)) return;
|
|
203
|
+
|
|
170
204
|
const currentValue = this.chargeInputs[field] ?? 0;
|
|
171
205
|
const newValue = currentValue + 1;
|
|
172
206
|
|
|
173
|
-
if (
|
|
174
|
-
field === "num_cats" ||
|
|
175
|
-
field === "num_dogs" ||
|
|
176
|
-
field === "num_other_pets"
|
|
177
|
-
) {
|
|
207
|
+
if (PET_FIELDS.includes(field)) {
|
|
178
208
|
this.updatePetCount({ [field]: newValue });
|
|
179
209
|
} else {
|
|
180
210
|
this.onChargeInputsChange?.({ [field]: newValue });
|
|
@@ -186,11 +216,7 @@ export class ChargeInputsComponent extends LitElement {
|
|
|
186
216
|
const minValue = field === "num_applicants" ? 1 : 0; // Minimum 1 applicant
|
|
187
217
|
const newValue = Math.max(minValue, currentValue - 1);
|
|
188
218
|
|
|
189
|
-
if (
|
|
190
|
-
field === "num_cats" ||
|
|
191
|
-
field === "num_dogs" ||
|
|
192
|
-
field === "num_other_pets"
|
|
193
|
-
) {
|
|
219
|
+
if (PET_FIELDS.includes(field)) {
|
|
194
220
|
this.updatePetCount({ [field]: newValue });
|
|
195
221
|
} else {
|
|
196
222
|
this.onChargeInputsChange?.({ [field]: newValue });
|
|
@@ -293,6 +319,7 @@ export class ChargeInputsComponent extends LitElement {
|
|
|
293
319
|
<button
|
|
294
320
|
class="charge-input-button"
|
|
295
321
|
@click=${() => this.handleIncrement(field)}
|
|
322
|
+
?disabled=${this.isAtPetLimit(field)}
|
|
296
323
|
aria-label="Increase ${this.getFieldLabel(field)}"
|
|
297
324
|
>
|
|
298
325
|
+
|
|
@@ -317,6 +344,13 @@ export class ChargeInputsComponent extends LitElement {
|
|
|
317
344
|
<div class="section-group">
|
|
318
345
|
<div class="section-subtitle">Household Details</div>
|
|
319
346
|
${householdFields.map((field) => this.renderChargeInputRow(field))}
|
|
347
|
+
${this.maxPets !== null
|
|
348
|
+
? html`
|
|
349
|
+
<div class="charge-input-note">
|
|
350
|
+
${formatPetLimit(this.maxPets)}
|
|
351
|
+
</div>
|
|
352
|
+
`
|
|
353
|
+
: ""}
|
|
320
354
|
</div>
|
|
321
355
|
|
|
322
356
|
${this.rentableItems.length > 0
|
|
@@ -106,6 +106,13 @@ export default css`
|
|
|
106
106
|
min-height: 300px;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
.quote-error-message {
|
|
110
|
+
max-width: 360px;
|
|
111
|
+
margin: 0 24px;
|
|
112
|
+
text-align: center;
|
|
113
|
+
line-height: 1.5;
|
|
114
|
+
}
|
|
115
|
+
|
|
109
116
|
@media (max-width: 767px) {
|
|
110
117
|
.fee-calculator-layout {
|
|
111
118
|
flex-direction: column;
|
package/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout.ts
CHANGED
|
@@ -13,9 +13,13 @@ import { Quote } from "../../model/quote";
|
|
|
13
13
|
import { FeeQuote } from "../../model/fee-quote";
|
|
14
14
|
import { FeeFrequency, MarketableFeeNew } from "../../model/marketable-fee-new";
|
|
15
15
|
import { AmountRange } from "../../model/amount-range";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
ChargeInputs,
|
|
18
|
+
QuoteError,
|
|
19
|
+
} from "../../../../services/fees/calculateQuote";
|
|
17
20
|
import { DesiredRentableItem } from "../../model/desired-rentable-item";
|
|
18
21
|
import { formatCurrency } from "../../../../utils";
|
|
22
|
+
import { formatPetLimit } from "../../utils/pet-limit-utils";
|
|
19
23
|
|
|
20
24
|
import "../floor-plan-selector/floor-plan-selector";
|
|
21
25
|
import "../fee-card/fee-card";
|
|
@@ -39,6 +43,9 @@ export class FeeCalculatorLayout extends LitElement {
|
|
|
39
43
|
@property({ type: Object })
|
|
40
44
|
quote: Quote | null = null;
|
|
41
45
|
|
|
46
|
+
@property({ type: Object })
|
|
47
|
+
quoteError: QuoteError | null = null;
|
|
48
|
+
|
|
42
49
|
@property({ type: Array })
|
|
43
50
|
layoutOptions: LayoutOption[] = [];
|
|
44
51
|
|
|
@@ -93,6 +100,9 @@ export class FeeCalculatorLayout extends LitElement {
|
|
|
93
100
|
onChargeInputsChange: ((newInputs: Partial<ChargeInputs>) => void) | null =
|
|
94
101
|
null;
|
|
95
102
|
|
|
103
|
+
@property({ type: Number })
|
|
104
|
+
maxPets: number | null = null;
|
|
105
|
+
|
|
96
106
|
@property({ type: Array })
|
|
97
107
|
selectedRentableItems: DesiredRentableItem[] = [];
|
|
98
108
|
|
|
@@ -244,6 +254,16 @@ export class FeeCalculatorLayout extends LitElement {
|
|
|
244
254
|
`;
|
|
245
255
|
}
|
|
246
256
|
|
|
257
|
+
getQuoteErrorMessage(error: QuoteError): string {
|
|
258
|
+
if (error.type === "feeLimit") {
|
|
259
|
+
const limit = Math.min(
|
|
260
|
+
...error.violations.map((violation) => violation.limit)
|
|
261
|
+
);
|
|
262
|
+
return `${formatPetLimit(limit)}. Remove a pet to see your estimate.`;
|
|
263
|
+
}
|
|
264
|
+
return "We couldn't calculate an estimate. Please adjust your selections or try again later.";
|
|
265
|
+
}
|
|
266
|
+
|
|
247
267
|
renderFeesContent(): TemplateResult {
|
|
248
268
|
if (!this.selectedUnit) {
|
|
249
269
|
return html`
|
|
@@ -253,6 +273,16 @@ export class FeeCalculatorLayout extends LitElement {
|
|
|
253
273
|
`;
|
|
254
274
|
}
|
|
255
275
|
|
|
276
|
+
if (this.quoteError && !this.showSkeletonLoader) {
|
|
277
|
+
return html`
|
|
278
|
+
<div class="fees-container empty-state" role="alert">
|
|
279
|
+
<p class="quote-error-message">
|
|
280
|
+
${this.getQuoteErrorMessage(this.quoteError)}
|
|
281
|
+
</p>
|
|
282
|
+
</div>
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
|
|
256
286
|
return html`
|
|
257
287
|
<div class="fees-container">
|
|
258
288
|
${this.showSkeletonLoader
|
|
@@ -293,6 +323,7 @@ export class FeeCalculatorLayout extends LitElement {
|
|
|
293
323
|
<charge-inputs
|
|
294
324
|
.chargeInputs=${this.chargeInputs}
|
|
295
325
|
.onChargeInputsChange=${this.onChargeInputsChange}
|
|
326
|
+
.maxPets=${this.maxPets}
|
|
296
327
|
.disabled=${!this.selectedUnit}
|
|
297
328
|
.rentableItems=${this.rentableItems}
|
|
298
329
|
.selectedRentableItems=${this.selectedRentableItems}
|
|
@@ -20,13 +20,14 @@ import { Unit } from "../../services/fees/fetchBuildingUnits";
|
|
|
20
20
|
import { RentableItemSummary } from "./model/rentable-item-summary";
|
|
21
21
|
import { TODAY } from "../../globals";
|
|
22
22
|
import { MarketableFee, MarketableFeeFrequency } from "./model/marketable-fee";
|
|
23
|
-
import { ChargeInputs } from "../../services/fees/calculateQuote";
|
|
23
|
+
import { ChargeInputs, QuoteError } from "../../services/fees/calculateQuote";
|
|
24
24
|
import { DesiredRentableItem } from "./model/desired-rentable-item";
|
|
25
25
|
import { sendLoggingEvent } from "../../analytics";
|
|
26
26
|
import { LogType } from "../../analytics";
|
|
27
27
|
import { pushGtmEvent } from "../../gtm";
|
|
28
28
|
import fetchBuildingUnits from "../../services/fees/fetchBuildingUnits";
|
|
29
29
|
import { layoutToBedrooms } from "../../services/fees/utils";
|
|
30
|
+
import { getMaxPets } from "./utils/pet-limit-utils";
|
|
30
31
|
|
|
31
32
|
import "./components";
|
|
32
33
|
import "./components/fee-calculator-layout/fee-calculator-layout";
|
|
@@ -112,6 +113,9 @@ export class FeeCalculator extends LitElement {
|
|
|
112
113
|
@state()
|
|
113
114
|
pdfUrl: string | null = null;
|
|
114
115
|
|
|
116
|
+
@state()
|
|
117
|
+
private quoteError: QuoteError | null = null;
|
|
118
|
+
|
|
115
119
|
@state()
|
|
116
120
|
private groupedFees: Record<MarketableFeeFrequency, MarketableFee[]> = {
|
|
117
121
|
[MarketableFeeFrequency.ONE_TIME]: [],
|
|
@@ -127,6 +131,9 @@ export class FeeCalculator extends LitElement {
|
|
|
127
131
|
@state()
|
|
128
132
|
private rentableItems: RentableItemSummary[] = [];
|
|
129
133
|
|
|
134
|
+
@state()
|
|
135
|
+
private maxPets: number | null = null;
|
|
136
|
+
|
|
130
137
|
@state()
|
|
131
138
|
private units: Unit[] = [];
|
|
132
139
|
|
|
@@ -150,13 +157,18 @@ export class FeeCalculator extends LitElement {
|
|
|
150
157
|
await this.performQuoteCalculation(this.quoteAbortController.signal);
|
|
151
158
|
};
|
|
152
159
|
|
|
160
|
+
private clearQuote(): void {
|
|
161
|
+
this.quote = null;
|
|
162
|
+
this.pdfUrl = null;
|
|
163
|
+
this.quoteError = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
153
166
|
/**
|
|
154
167
|
* Performs the actual quote calculation API request.
|
|
155
168
|
*/
|
|
156
169
|
private async performQuoteCalculation(signal: AbortSignal): Promise<void> {
|
|
157
170
|
if (!this.selectedUnit || !this.moveInDateString) {
|
|
158
|
-
this.
|
|
159
|
-
this.pdfUrl = null;
|
|
171
|
+
this.clearQuote();
|
|
160
172
|
this.isCalculatingQuote = false;
|
|
161
173
|
return;
|
|
162
174
|
}
|
|
@@ -179,8 +191,11 @@ export class FeeCalculator extends LitElement {
|
|
|
179
191
|
},
|
|
180
192
|
signal
|
|
181
193
|
);
|
|
194
|
+
if (signal.aborted) return;
|
|
195
|
+
|
|
182
196
|
this.quote = res.quote;
|
|
183
197
|
this.pdfUrl = res.pdfUrl || null;
|
|
198
|
+
this.quoteError = res.error ?? null;
|
|
184
199
|
if (this.quote) {
|
|
185
200
|
pushGtmEvent("feeCalculatorQuoteGenerated", {
|
|
186
201
|
buildingSlug: this.buildingSlug,
|
|
@@ -192,6 +207,7 @@ export class FeeCalculator extends LitElement {
|
|
|
192
207
|
if ((error as DOMException)?.name !== "AbortError") {
|
|
193
208
|
this.quote = null;
|
|
194
209
|
this.pdfUrl = null;
|
|
210
|
+
this.quoteError = { type: "unknown" };
|
|
195
211
|
this.isCalculatingQuote = false;
|
|
196
212
|
sendLoggingEvent({
|
|
197
213
|
logType: LogType.error,
|
|
@@ -242,6 +258,7 @@ export class FeeCalculator extends LitElement {
|
|
|
242
258
|
(response) => {
|
|
243
259
|
if (!response) return;
|
|
244
260
|
this.groupedFees = this.groupFees(response.fees);
|
|
261
|
+
this.maxPets = getMaxPets(response.fees);
|
|
245
262
|
this.rentableItems = response.rentableItems;
|
|
246
263
|
// TODO(Leo): Re-enable
|
|
247
264
|
// this.incentives = response.buildingIncentives;
|
|
@@ -283,8 +300,7 @@ export class FeeCalculator extends LitElement {
|
|
|
283
300
|
}
|
|
284
301
|
|
|
285
302
|
if (!this.selectedUnit || !this.moveInDateString) {
|
|
286
|
-
this.
|
|
287
|
-
this.pdfUrl = null;
|
|
303
|
+
this.clearQuote();
|
|
288
304
|
this.isCalculatingQuote = false;
|
|
289
305
|
return;
|
|
290
306
|
}
|
|
@@ -319,8 +335,7 @@ export class FeeCalculator extends LitElement {
|
|
|
319
335
|
this.moveInDateString = dateString;
|
|
320
336
|
|
|
321
337
|
// Clear old calculation and show loading state immediately
|
|
322
|
-
this.
|
|
323
|
-
this.pdfUrl = null;
|
|
338
|
+
this.clearQuote();
|
|
324
339
|
|
|
325
340
|
await this.fetchUnits();
|
|
326
341
|
|
|
@@ -334,8 +349,7 @@ export class FeeCalculator extends LitElement {
|
|
|
334
349
|
this.leaseTerm = leaseTerm;
|
|
335
350
|
|
|
336
351
|
// Clear old calculation and show loading state immediately
|
|
337
|
-
this.
|
|
338
|
-
this.pdfUrl = null;
|
|
352
|
+
this.clearQuote();
|
|
339
353
|
|
|
340
354
|
await this.fetchUnits();
|
|
341
355
|
|
|
@@ -443,8 +457,7 @@ export class FeeCalculator extends LitElement {
|
|
|
443
457
|
|
|
444
458
|
if (!isUnitStillAvailable) {
|
|
445
459
|
this.selectedUnit = null;
|
|
446
|
-
this.
|
|
447
|
-
this.pdfUrl = null;
|
|
460
|
+
this.clearQuote();
|
|
448
461
|
}
|
|
449
462
|
}
|
|
450
463
|
} finally {
|
|
@@ -530,6 +543,7 @@ export class FeeCalculator extends LitElement {
|
|
|
530
543
|
.groupedFees=${this.groupedFees}
|
|
531
544
|
.rentableItems=${this.rentableItems}
|
|
532
545
|
.quote=${this.quote}
|
|
546
|
+
.quoteError=${this.quoteError}
|
|
533
547
|
.layoutOptions=${this.layouts}
|
|
534
548
|
.selectedLayoutIds=${this.selectedLayoutIds}
|
|
535
549
|
.units=${this.units}
|
|
@@ -541,6 +555,7 @@ export class FeeCalculator extends LitElement {
|
|
|
541
555
|
.onLeaseTermChange=${this.handleLeaseTermChange}
|
|
542
556
|
.chargeInputs=${this.chargeInputs}
|
|
543
557
|
.onChargeInputsChange=${this.handleChargeInputsChange}
|
|
558
|
+
.maxPets=${this.maxPets}
|
|
544
559
|
.selectedRentableItems=${this.desiredRentableItems}
|
|
545
560
|
.onRentableItemAdd=${this.handleRentableItemAdd}
|
|
546
561
|
.onRentableItemRemove=${this.handleRentableItemRemove}
|
|
@@ -31,6 +31,7 @@ export interface MarketableFee {
|
|
|
31
31
|
fee_name: string;
|
|
32
32
|
amount?: number;
|
|
33
33
|
amount_display: string;
|
|
34
|
+
limit?: number | null;
|
|
34
35
|
|
|
35
36
|
min_estimate?: number;
|
|
36
37
|
max_estimate?: number;
|
|
@@ -64,6 +65,7 @@ export class MarketableFeeModel implements MarketableFee {
|
|
|
64
65
|
fee_name: string;
|
|
65
66
|
amount?: number;
|
|
66
67
|
amount_display: string;
|
|
68
|
+
limit: number | null;
|
|
67
69
|
min_estimate?: number;
|
|
68
70
|
max_estimate?: number;
|
|
69
71
|
percent_rent?: number;
|
|
@@ -90,6 +92,7 @@ export class MarketableFeeModel implements MarketableFee {
|
|
|
90
92
|
this.fee_name = data.fee_name ?? "";
|
|
91
93
|
this.amount = data.amount;
|
|
92
94
|
this.amount_display = data.amount_display ?? "";
|
|
95
|
+
this.limit = data.limit ?? null;
|
|
93
96
|
this.min_estimate = data.min_estimate;
|
|
94
97
|
this.max_estimate = data.max_estimate;
|
|
95
98
|
this.percent_rent = data.percent_rent;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MarketableFee, MarketableFeeCategory } from "../model/marketable-fee";
|
|
2
|
+
|
|
3
|
+
const PET_FEE_CATEGORIES: MarketableFeeCategory[] = [
|
|
4
|
+
MarketableFeeCategory.PetFee,
|
|
5
|
+
MarketableFeeCategory.PetRent,
|
|
6
|
+
MarketableFeeCategory.PetDeposit,
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Gets the most pets a quote can include, or null when no pet fee has a limit.
|
|
11
|
+
* The quote counts every pet against each pet fee's limit, so the lowest limit caps the total.
|
|
12
|
+
*/
|
|
13
|
+
export function getMaxPets(fees: MarketableFee[]): number | null {
|
|
14
|
+
const limits = fees
|
|
15
|
+
.filter((fee) => PET_FEE_CATEGORIES.includes(fee.category))
|
|
16
|
+
.map((fee) => fee.limit)
|
|
17
|
+
.filter((limit): limit is number => typeof limit === "number");
|
|
18
|
+
|
|
19
|
+
return limits.length ? Math.min(...limits) : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Describes a pet limit, for example "Up to 2 pets allowed".
|
|
24
|
+
*/
|
|
25
|
+
export function formatPetLimit(maxPets: number): string {
|
|
26
|
+
return `Up to ${maxPets} ${maxPets === 1 ? "pet" : "pets"} allowed`;
|
|
27
|
+
}
|