@meetelise/chat 1.55.1 → 1.55.3
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/utils/date-utils.d.ts +9 -0
- package/dist/src/globals.d.ts +0 -1
- package/package.json +1 -1
- package/public/dist/index.js +238 -237
- package/src/WebComponent/FeeCalculator/components/floor-plan-selector/floor-plan-selector.ts +21 -7
- package/src/WebComponent/FeeCalculator/fee-calculator.ts +2 -2
- package/src/WebComponent/FeeCalculator/utils/date-utils.ts +17 -0
- package/src/feeCalculatorLeaseTerm.test.ts +71 -0
- package/src/feeCalculatorMoveInDate.test.ts +113 -0
- package/src/globals.ts +0 -4
- package/src/services/fees/buildBuildingUnitsQuery.ts +4 -1
- package/src/services/fees/calculateQuote.ts +1 -1
- package/src/services/fees/fetchBuildingUnits.test.ts +20 -0
package/src/WebComponent/FeeCalculator/components/floor-plan-selector/floor-plan-selector.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import isBefore from "date-fns/isBefore";
|
|
2
|
+
import startOfToday from "date-fns/startOfToday";
|
|
1
3
|
import { LitElement, html, TemplateResult } from "lit";
|
|
2
4
|
import { customElement, property, state } from "lit/decorators.js";
|
|
3
5
|
import { LayoutOption } from "../../../../fetchBuildingWebchatView";
|
|
4
6
|
import { ALL_LAYOUTS_OPTION, DEFAULT_LEASE_TERM } from "../../constants";
|
|
5
7
|
import { Unit } from "../../../../services/fees/fetchBuildingUnits";
|
|
6
8
|
import floorPlanSelectorStyles from "./floor-plan-selector-styles";
|
|
7
|
-
import {
|
|
9
|
+
import { formatLocalDate, getTodayString } from "../../utils/date-utils";
|
|
8
10
|
|
|
9
11
|
import "../floorplan-image-card/floorplan-image-card";
|
|
10
12
|
|
|
@@ -46,7 +48,7 @@ export class FloorPlanSelector extends LitElement {
|
|
|
46
48
|
onLeaseTermChange: ((leaseTerm: number) => void) | null = null;
|
|
47
49
|
|
|
48
50
|
@state()
|
|
49
|
-
moveInDate: Date | null =
|
|
51
|
+
moveInDate: Date | null = startOfToday();
|
|
50
52
|
|
|
51
53
|
@state()
|
|
52
54
|
leaseTerm = DEFAULT_LEASE_TERM;
|
|
@@ -126,19 +128,28 @@ export class FloorPlanSelector extends LitElement {
|
|
|
126
128
|
handleMoveInDateChange = (e: Event): void => {
|
|
127
129
|
const inputElement = e.target as HTMLInputElement;
|
|
128
130
|
const inputValue = inputElement.value;
|
|
131
|
+
const today = startOfToday();
|
|
129
132
|
|
|
130
133
|
if (!inputValue) {
|
|
131
|
-
this.moveInDate =
|
|
134
|
+
this.moveInDate = today;
|
|
132
135
|
} else {
|
|
133
136
|
const [year, month, day] = inputValue.split("-").map(Number);
|
|
134
|
-
|
|
137
|
+
const moveInDate = new Date(year, month - 1, day);
|
|
138
|
+
// Typing skips `min`, and Chrome reports each digit of a typed year (0002, 0020...) as a date.
|
|
139
|
+
// Ignore past dates instead of resetting the input mid-typing.
|
|
140
|
+
if (isBefore(moveInDate, today)) return;
|
|
141
|
+
this.moveInDate = moveInDate;
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
this.onMoveInDateChange?.(inputValue ||
|
|
144
|
+
this.onMoveInDateChange?.(inputValue || formatLocalDate(today));
|
|
138
145
|
};
|
|
139
146
|
|
|
140
147
|
handleLeaseTermChange = (e: Event): void => {
|
|
141
|
-
|
|
148
|
+
const leaseTerm = parseInt((e.target as HTMLInputElement).value);
|
|
149
|
+
// Clearing the box to retype it reports an empty value (NaN).
|
|
150
|
+
// Keep the last valid lease term instead of reloading units mid-edit.
|
|
151
|
+
if (!Number.isInteger(leaseTerm) || leaseTerm <= 0) return;
|
|
152
|
+
this.leaseTerm = leaseTerm;
|
|
142
153
|
this.onLeaseTermChange?.(this.leaseTerm);
|
|
143
154
|
};
|
|
144
155
|
|
|
@@ -225,7 +236,10 @@ export class FloorPlanSelector extends LitElement {
|
|
|
225
236
|
<input
|
|
226
237
|
type="date"
|
|
227
238
|
placeholder="Select move in date"
|
|
228
|
-
|
|
239
|
+
min=${getTodayString()}
|
|
240
|
+
.value=${this.moveInDate
|
|
241
|
+
? formatLocalDate(this.moveInDate)
|
|
242
|
+
: ""}
|
|
229
243
|
@input=${this.handleMoveInDateChange}
|
|
230
244
|
/>
|
|
231
245
|
</div>
|
|
@@ -18,7 +18,7 @@ import { XOutlineIcon } from "../icons/XOutlineIcon";
|
|
|
18
18
|
import { IncentiveV2 } from "../../types/incentive-v2";
|
|
19
19
|
import { Unit } from "../../services/fees/fetchBuildingUnits";
|
|
20
20
|
import { RentableItemSummary } from "./model/rentable-item-summary";
|
|
21
|
-
import {
|
|
21
|
+
import { getTodayString } from "./utils/date-utils";
|
|
22
22
|
import { MarketableFee, MarketableFeeFrequency } from "./model/marketable-fee";
|
|
23
23
|
import { ChargeInputs, QuoteError } from "../../services/fees/calculateQuote";
|
|
24
24
|
import { DesiredRentableItem } from "./model/desired-rentable-item";
|
|
@@ -96,7 +96,7 @@ export class FeeCalculator extends LitElement {
|
|
|
96
96
|
private desiredRentableItems: DesiredRentableItem[] = [];
|
|
97
97
|
|
|
98
98
|
@state()
|
|
99
|
-
private moveInDateString: string =
|
|
99
|
+
private moveInDateString: string = getTodayString();
|
|
100
100
|
|
|
101
101
|
@state()
|
|
102
102
|
private leaseTerm = DEFAULT_LEASE_TERM;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import format from "date-fns/format";
|
|
2
|
+
import startOfToday from "date-fns/startOfToday";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Formats a date as its local `yyyy-MM-dd` calendar date, the value format of a date input.
|
|
6
|
+
* `toISOString()` gives the UTC date instead, which can be a day off.
|
|
7
|
+
*/
|
|
8
|
+
export function formatLocalDate(date: Date): string {
|
|
9
|
+
return format(date, "yyyy-MM-dd");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gets the prospect's local date today as `yyyy-MM-dd`.
|
|
14
|
+
*/
|
|
15
|
+
export function getTodayString(): string {
|
|
16
|
+
return formatLocalDate(startOfToday());
|
|
17
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { expect } from "@esm-bundle/chai";
|
|
2
|
+
import "../public/dist/index";
|
|
3
|
+
import type { FloorPlanSelector } from "./WebComponent/FeeCalculator/components/floor-plan-selector/floor-plan-selector";
|
|
4
|
+
|
|
5
|
+
describe("floor-plan-selector lease term", () => {
|
|
6
|
+
let selector: FloorPlanSelector;
|
|
7
|
+
let sentLeaseTerms: number[];
|
|
8
|
+
|
|
9
|
+
const getLeaseTermInput = (): HTMLInputElement => {
|
|
10
|
+
const input = selector.shadowRoot?.querySelector<HTMLInputElement>(
|
|
11
|
+
'input[type="number"]'
|
|
12
|
+
);
|
|
13
|
+
if (!input) throw new Error("Expected lease term input");
|
|
14
|
+
return input;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const enterLeaseTerm = async (value: string): Promise<void> => {
|
|
18
|
+
const input = getLeaseTermInput();
|
|
19
|
+
input.value = value;
|
|
20
|
+
input.dispatchEvent(new Event("input"));
|
|
21
|
+
await selector.updateComplete;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
sentLeaseTerms = [];
|
|
26
|
+
selector = document.createElement(
|
|
27
|
+
"floor-plan-selector"
|
|
28
|
+
) as FloorPlanSelector;
|
|
29
|
+
selector.onLeaseTermChange = (leaseTerm) => sentLeaseTerms.push(leaseTerm);
|
|
30
|
+
document.body.appendChild(selector);
|
|
31
|
+
await selector.updateComplete;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
selector.remove();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("starts on the default lease term", () => {
|
|
39
|
+
expect(getLeaseTermInput().value).to.equal("12");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("sends each lease term the prospect types", async () => {
|
|
43
|
+
await enterLeaseTerm("1");
|
|
44
|
+
await enterLeaseTerm("18");
|
|
45
|
+
|
|
46
|
+
expect(sentLeaseTerms).to.deep.equal([1, 18]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("keeps the box empty without sending anything while it is retyped", async () => {
|
|
50
|
+
await enterLeaseTerm("");
|
|
51
|
+
// A rerender, for example from new units arriving, must not refill the box.
|
|
52
|
+
selector.requestUpdate();
|
|
53
|
+
await selector.updateComplete;
|
|
54
|
+
|
|
55
|
+
expect(getLeaseTermInput().value).to.equal("");
|
|
56
|
+
expect(sentLeaseTerms).to.deep.equal([]);
|
|
57
|
+
|
|
58
|
+
await enterLeaseTerm("1");
|
|
59
|
+
await enterLeaseTerm("18");
|
|
60
|
+
|
|
61
|
+
expect(getLeaseTermInput().value).to.equal("18");
|
|
62
|
+
expect(sentLeaseTerms).to.deep.equal([1, 18]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("ignores lease terms below one month", async () => {
|
|
66
|
+
await enterLeaseTerm("0");
|
|
67
|
+
await enterLeaseTerm("-6");
|
|
68
|
+
|
|
69
|
+
expect(sentLeaseTerms).to.deep.equal([]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { expect } from "@esm-bundle/chai";
|
|
2
|
+
import {
|
|
3
|
+
restore,
|
|
4
|
+
useFakeTimers,
|
|
5
|
+
useFakeXMLHttpRequest,
|
|
6
|
+
} from "sinon/pkg/sinon-esm";
|
|
7
|
+
import "../public/dist/index";
|
|
8
|
+
import type { FloorPlanSelector } from "./WebComponent/FeeCalculator/components/floor-plan-selector/floor-plan-selector";
|
|
9
|
+
import type { FeeCalculator } from "./WebComponent/FeeCalculator/fee-calculator";
|
|
10
|
+
import { formatLocalDate } from "./WebComponent/FeeCalculator/utils/date-utils";
|
|
11
|
+
|
|
12
|
+
type FakeRequest = XMLHttpRequest & { url: string };
|
|
13
|
+
|
|
14
|
+
// Late evening, and far from the real date, so a date read at module load cannot match.
|
|
15
|
+
const NOW = new Date(2030, 4, 20, 23, 30);
|
|
16
|
+
const TODAY = "2030-05-20";
|
|
17
|
+
|
|
18
|
+
const waitFor = async <T>(
|
|
19
|
+
find: () => T | undefined,
|
|
20
|
+
description: string
|
|
21
|
+
): Promise<T> => {
|
|
22
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
23
|
+
const found = find();
|
|
24
|
+
if (found) return found;
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Timed out waiting for ${description}`);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
useFakeTimers({ now: NOW, toFake: ["Date"] });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
restore();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("formats the local calendar date at both ends of the day", () => {
|
|
39
|
+
expect(formatLocalDate(new Date(2030, 4, 20, 0, 0))).to.equal(TODAY);
|
|
40
|
+
expect(formatLocalDate(new Date(2030, 4, 20, 23, 59))).to.equal(TODAY);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("floor-plan-selector move-in date", () => {
|
|
44
|
+
let selector: FloorPlanSelector;
|
|
45
|
+
let sentDates: string[];
|
|
46
|
+
|
|
47
|
+
const getDateInput = (): HTMLInputElement => {
|
|
48
|
+
const input =
|
|
49
|
+
selector.shadowRoot?.querySelector<HTMLInputElement>(
|
|
50
|
+
'input[type="date"]'
|
|
51
|
+
);
|
|
52
|
+
if (!input) throw new Error("Expected move-in date input");
|
|
53
|
+
return input;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const enterDate = async (value: string): Promise<void> => {
|
|
57
|
+
const input = getDateInput();
|
|
58
|
+
input.value = value;
|
|
59
|
+
input.dispatchEvent(new Event("input"));
|
|
60
|
+
await selector.updateComplete;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
sentDates = [];
|
|
65
|
+
selector = document.createElement(
|
|
66
|
+
"floor-plan-selector"
|
|
67
|
+
) as FloorPlanSelector;
|
|
68
|
+
selector.onMoveInDateChange = (date) => sentDates.push(date);
|
|
69
|
+
document.body.appendChild(selector);
|
|
70
|
+
await selector.updateComplete;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
selector.remove();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("starts on today and does not allow earlier dates", () => {
|
|
78
|
+
expect(getDateInput().value).to.equal(TODAY);
|
|
79
|
+
expect(getDateInput().min).to.equal(TODAY);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("shows and sends the date the prospect picks", async () => {
|
|
83
|
+
await enterDate("2030-06-01");
|
|
84
|
+
|
|
85
|
+
expect(getDateInput().value).to.equal("2030-06-01");
|
|
86
|
+
expect(sentDates).to.deep.equal(["2030-06-01"]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("ignores typed dates before today", async () => {
|
|
90
|
+
// Chrome reports each digit of a typed year as a date, for example 0002-06-01.
|
|
91
|
+
await enterDate("2030-05-19");
|
|
92
|
+
await enterDate("0002-06-01");
|
|
93
|
+
|
|
94
|
+
expect(sentDates).to.deep.equal([]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("requests units for today's local date by default", async () => {
|
|
99
|
+
const fakeXhr = useFakeXMLHttpRequest();
|
|
100
|
+
const requests: FakeRequest[] = [];
|
|
101
|
+
fakeXhr.onCreate = (request: FakeRequest) => requests.push(request);
|
|
102
|
+
const calculator = document.createElement("fee-calculator") as FeeCalculator;
|
|
103
|
+
calculator.buildingSlug = "test-building";
|
|
104
|
+
document.body.appendChild(calculator);
|
|
105
|
+
|
|
106
|
+
const unitsRequest = await waitFor(
|
|
107
|
+
() => requests.find((request) => request.url.includes("/units")),
|
|
108
|
+
"the units request"
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(unitsRequest.url).to.include(`move_in_date_earliest=${TODAY}`);
|
|
112
|
+
calculator.remove();
|
|
113
|
+
});
|
package/src/globals.ts
CHANGED
|
@@ -19,7 +19,10 @@ export const buildBuildingUnitsQuery = ({
|
|
|
19
19
|
)
|
|
20
20
|
.addSingle("move_in_date_earliest", moveInDateString)
|
|
21
21
|
.addSingle("move_in_date_latest", moveInDateString)
|
|
22
|
-
.addSingle(
|
|
22
|
+
.addSingle(
|
|
23
|
+
"lease_term_min",
|
|
24
|
+
Number.isFinite(leaseTerm) ? leaseTerm?.toString() : undefined
|
|
25
|
+
);
|
|
23
26
|
|
|
24
27
|
return queryParamBuilder.build();
|
|
25
28
|
};
|
|
@@ -16,6 +16,26 @@ it("requests units for the selected move-in date", () => {
|
|
|
16
16
|
expect(queryString).to.include("lease_term_min=12");
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
it("leaves out a lease term that is not a number", () => {
|
|
20
|
+
const queryString = buildBuildingUnitsQuery({
|
|
21
|
+
moveInDateString: "2026-08-01",
|
|
22
|
+
leaseTerm: NaN,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(queryString).to.include("move_in_date_earliest=2026-08-01");
|
|
26
|
+
expect(queryString).not.to.include("lease_term_min");
|
|
27
|
+
expect(queryString).not.to.include("NaN");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("leaves out the lease term when none is given", () => {
|
|
31
|
+
const queryString = buildBuildingUnitsQuery({
|
|
32
|
+
moveInDateString: "2026-08-01",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(queryString).to.include("move_in_date_earliest=2026-08-01");
|
|
36
|
+
expect(queryString).not.to.include("lease_term_min");
|
|
37
|
+
});
|
|
38
|
+
|
|
19
39
|
it("sorts the cheapest units first without mutating the API response", () => {
|
|
20
40
|
const units = [
|
|
21
41
|
{ id: 3, startingPrice: 2699 },
|