@meetelise/chat 1.54.1 → 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.
Files changed (25) hide show
  1. package/dist/src/WebComponent/FeeCalculator/components/charge-inputs/charge-inputs.d.ts +3 -0
  2. package/dist/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout.d.ts +4 -1
  3. package/dist/src/WebComponent/FeeCalculator/fee-calculator.d.ts +3 -0
  4. package/dist/src/WebComponent/FeeCalculator/model/marketable-fee.d.ts +2 -0
  5. package/dist/src/WebComponent/FeeCalculator/utils/pet-limit-utils.d.ts +10 -0
  6. package/dist/src/WebComponent/Scheduler/tour-scheduler.d.ts +21 -0
  7. package/dist/src/WebComponent/actions/email-us-window.d.ts +1 -7
  8. package/dist/src/WebComponent/brokerReferral.d.ts +17 -0
  9. package/dist/src/services/fees/calculateQuote.d.ts +14 -1
  10. package/package.json +1 -1
  11. package/public/dist/index.js +505 -284
  12. package/src/WebComponent/FeeCalculator/components/charge-inputs/charge-inputs.ts +44 -10
  13. package/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout-styles.ts +7 -0
  14. package/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout.ts +32 -1
  15. package/src/WebComponent/FeeCalculator/fee-calculator.ts +26 -11
  16. package/src/WebComponent/FeeCalculator/model/marketable-fee.ts +3 -0
  17. package/src/WebComponent/FeeCalculator/utils/pet-limit-utils.ts +27 -0
  18. package/src/WebComponent/Scheduler/tour-scheduler.ts +253 -32
  19. package/src/WebComponent/Scheduler/tourSchedulerStyles.ts +67 -1
  20. package/src/WebComponent/actions/email-us-window.ts +72 -51
  21. package/src/WebComponent/brokerReferral.ts +55 -0
  22. package/src/WebComponent/launcher/Launcher.ts +1 -0
  23. package/src/brokerReferralSubmission.test.ts +149 -0
  24. package/src/feeCalculatorPetLimit.test.ts +346 -0
  25. 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;
@@ -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 { ChargeInputs } from "../../../../services/fees/calculateQuote";
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.quote = null;
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.quote = null;
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.quote = null;
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.quote = null;
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.quote = null;
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
+ }