toga-ai 1.0.167 → 1.0.169
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/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +107 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/clickup-work-type-automation.md +80 -0
- package/knowledge/INDEX.md +3 -3
- package/knowledge/clients/rate/INDEX.md +1 -0
- package/knowledge/clients/rate/features/service-card-entitlements.md +103 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
|
|
6
|
+
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Page.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql |
|
|
6
7
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
7
8
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
8
9
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ACL Permission Chain (Record & Field Authorization)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-23
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- api2/Component/Api/V2/V2.php
|
|
13
|
+
- _underscore/Model/Core/Page.php
|
|
14
|
+
- dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql
|
|
15
|
+
- dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete
|
|
21
|
+
a record is decided by rows across **four linked tables**, not by code. Granting access by
|
|
22
|
+
inserting only an `AclRecordPermissions` row is the single most common mistake — the permission
|
|
23
|
+
silently does nothing without the rest of the chain. A complete grant requires **all four** tables,
|
|
24
|
+
plus `AclFieldPermissions` for the fields to be readable/writable.
|
|
25
|
+
|
|
26
|
+
## The complete chain (all four are required)
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
AclRecordPermissions → AclLogicGroups → AclLogicGroupExpressions → AclRecordExpressions
|
|
30
|
+
(the role × record (operator AND/OR, (binds a group to an (the SQL test,
|
|
31
|
+
CRUD grant) parent group tree) expression) e.g. '1' = all)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
1. **`AclRecordPermissions`** — the grant: `recordId`, `roleId`, `allowCreate/Read/Update/Delete`
|
|
35
|
+
(optional `appId`, `indirectRecordId`). Unique on `(appId, recordId, roleId, indirectRecordId)`.
|
|
36
|
+
2. **`AclLogicGroups`** — at least one root group per permission: `aclRecordPermissionId` (FK),
|
|
37
|
+
`operator` **`enum('AND','OR') NOT NULL`** (this column is mandatory and easy to forget),
|
|
38
|
+
optional `parentAclLogicGroupId` for nested groups.
|
|
39
|
+
3. **`AclLogicGroupExpressions`** — binds a logic group to a record expression: `aclLogicGroupId`
|
|
40
|
+
(FK), `aclRecordExpressionId` (FK). Unique on `(aclLogicGroupId, aclRecordExpressionId)`.
|
|
41
|
+
4. **`AclRecordExpressions`** — the actual SQL test: `recordId`, `slug`, `description`,
|
|
42
|
+
`sqlExpression` (TEXT). Unique on `(recordId, slug)`. The conventional "always allow" row is
|
|
43
|
+
`slug = 'all'`, `sqlExpression = '1'`.
|
|
44
|
+
|
|
45
|
+
**Field visibility — `AclFieldPermissions`.** Record-level access alone does **not** expose
|
|
46
|
+
fields. Each field needs a row: `recordFieldId`, `roleId`, `isWritable` (0 = read-only, 1 =
|
|
47
|
+
writable). Without it the field is omitted from responses (and rejected on write).
|
|
48
|
+
|
|
49
|
+
## Where ACL rows live: Core vs. Client database
|
|
50
|
+
|
|
51
|
+
`Core.Records.aclDatabase` decides which database holds the ACL rows for that record:
|
|
52
|
+
|
|
53
|
+
- **`aclDatabase = 'CORE'`** → ACL rows live in the **Core** database.
|
|
54
|
+
- **`aclDatabase = 'CLIENT'`** → ACL rows live in **each client's own database**. A migration
|
|
55
|
+
granting access therefore goes in `dbchanges2/Client/` (or a specific `Client_<Name>/`) so it
|
|
56
|
+
runs against every client DB.
|
|
57
|
+
|
|
58
|
+
**Role ids differ per client DB**, so never hardcode a `roleId` in a `CLIENT` ACL migration —
|
|
59
|
+
resolve it with a subselect, e.g. the Base role:
|
|
60
|
+
|
|
61
|
+
```sql
|
|
62
|
+
roleId = (SELECT id FROM Roles WHERE `name` = 'Base')
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Canonical example (grant the Base role full CRUD on a record)
|
|
66
|
+
|
|
67
|
+
```sql
|
|
68
|
+
INSERT INTO AclRecordPermissions SET
|
|
69
|
+
uuid = '…', recordId = 331, roleId = (SELECT id FROM Roles WHERE `name` = 'Base'),
|
|
70
|
+
allowCreate = 1, allowRead = 1, allowUpdate = 1, allowDelete = 1;
|
|
71
|
+
|
|
72
|
+
INSERT INTO AclRecordExpressions SET
|
|
73
|
+
uuid = '…', recordId = 331, slug = 'all', description = 'All', sqlExpression = '1';
|
|
74
|
+
|
|
75
|
+
INSERT INTO AclLogicGroups SET
|
|
76
|
+
uuid = '…', aclRecordPermissionId = (SELECT MAX(id) FROM AclRecordPermissions), operator = 'AND';
|
|
77
|
+
|
|
78
|
+
INSERT INTO AclLogicGroupExpressions SET
|
|
79
|
+
uuid = '…', aclLogicGroupId = (SELECT MAX(id) FROM AclLogicGroups),
|
|
80
|
+
aclRecordExpressionId = (SELECT id FROM AclRecordExpressions WHERE recordId = 331 AND slug = 'all');
|
|
81
|
+
|
|
82
|
+
-- and one AclFieldPermissions row per exposed field (recordFieldId, roleId, isWritable)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## How it is enforced
|
|
86
|
+
|
|
87
|
+
`_Component_Api_V2::buildLookups()` bulk-loads these tables (from the Core and/or Client ACL
|
|
88
|
+
database per `Records.aclDatabase`) into in-memory lookups once per request; `processRoutePairs()`
|
|
89
|
+
then checks the caller's roles against `AclRecordPermissions` (→ `EZ-1` if no grant) and computes
|
|
90
|
+
readable/writable fields from `AclFieldPermissions`. `_Model_Core_Page::meta()` reads the same
|
|
91
|
+
tables to return per-page ACL to the frontend.
|
|
92
|
+
|
|
93
|
+
## Checklist (so the chain is never half-built)
|
|
94
|
+
|
|
95
|
+
- [ ] `AclRecordPermissions` row for the role × record with the right CRUD flags.
|
|
96
|
+
- [ ] `AclRecordExpressions` row (`slug='all'`, `sqlExpression='1'` for unconditional access).
|
|
97
|
+
- [ ] `AclLogicGroups` row with `operator` set (AND/OR) referencing the permission.
|
|
98
|
+
- [ ] `AclLogicGroupExpressions` row binding the group to the expression.
|
|
99
|
+
- [ ] `AclFieldPermissions` rows for every field that must be readable/writable.
|
|
100
|
+
- [ ] For `aclDatabase = 'CLIENT'` records: rows go in client DB(s); resolve `roleId` by subselect.
|
|
101
|
+
|
|
102
|
+
## Change history
|
|
103
|
+
|
|
104
|
+
- **2026-06-23** — Documented after repeatedly missing the logic-group/expression rows when
|
|
105
|
+
granting access to new records. Created alongside the `item-translations` record (331) ACL,
|
|
106
|
+
whose grant in `dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql` is a worked example of
|
|
107
|
+
the full chain for a `CLIENT`-aclDatabase record targeting the Base role.
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Worker (worker2) Architecture](architecture.md) | Worker (repo `worker2`) is an AWS Elastic Beanstalk **Worker Tier** application that processes background jobs. | worker2/Controller/Index.php, worker2/Worker/, worker2/LambdaFunctions/, _underscore/Worker.php |
|
|
6
6
|
| [ClickUp Project & Opportunity Multi-List Routing](features/clickup-project-routing.md) | Routes ClickUp tasks into the correct **secondary multi-list memberships** based on their custom-field values, via the `clickup` webhook. | worker2/Worker/Clickup/Project.php, worker2/Worker/Clickup.php |
|
|
7
|
+
| [ClickUp Work Type Automation (Committed / Conditional / Stretch)](features/clickup-work-type-automation.md) | The ClickUp webhook handler (`_Worker_Clickup`) automatically maintains each task's **Work Type** custom field — `Committed`, `Conditional`, or `Stretch` — base | worker2/Worker/Clickup.php, worker2/Tests/Worker/ClickupWorkTypeTest.php |
|
|
7
8
|
| [Creating Worker Actions](features/creating-worker-actions.md) | How to add a new callable Worker action — a PHP class whose `public static` methods are invoked as background jobs (via webhook, cron, or `_Worker::runTask()`). | worker2/Worker/, worker2/Controller/Index.php, _underscore/Worker.php |
|
|
8
9
|
| [Elite Freshservice Sync (worker2)](features/elite-freshservice-sync.md) | `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2. | worker2/Worker/Elite.php, worker2/Config/dev-kmaramreddy-laptop.ini |
|
|
9
10
|
| [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Notification/Email.php, dbchanges2/Core/2026-05-21 - Monitors.sql |
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ClickUp Work Type Automation (Committed / Conditional / Stretch)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-23
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Clickup.php
|
|
13
|
+
- worker2/Tests/Worker/ClickupWorkTypeTest.php
|
|
14
|
+
related:
|
|
15
|
+
- ./clickup-project-routing.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
The ClickUp webhook handler (`_Worker_Clickup`) automatically maintains each task's **Work
|
|
20
|
+
Type** custom field — `Committed`, `Conditional`, or `Stretch` — based on the task's
|
|
21
|
+
dependencies, its status, and its due date. The core rule: a task that should be Committed is
|
|
22
|
+
demoted to **Conditional** while it still has an unfinished dependency, and promoted back to
|
|
23
|
+
**Committed** once all its dependencies are complete. The logic lives in
|
|
24
|
+
`updateTaskAndDependencies()`.
|
|
25
|
+
|
|
26
|
+
## Key files / entry points
|
|
27
|
+
- `Worker/Clickup.php` → `Webhook($payload, $headers)` — webhook entry; routes by
|
|
28
|
+
`$payload->event`.
|
|
29
|
+
- `updateTaskAndDependencies($taskId, $dependsOnTaskId = null)` — the work-type engine.
|
|
30
|
+
- `getWorkType($taskDetails)` — reads the current Work Type custom field value.
|
|
31
|
+
- `isStatusComplete(string $statusType): bool` — true when a ClickUp status type is terminal.
|
|
32
|
+
- `getTaskDetails($taskId)` — cached `GET /task/{id}` (static per-invocation cache).
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
1. A ClickUp webhook hits the worker; `Webhook()` switches on `$payload->event`.
|
|
36
|
+
2. `updateTaskAndDependencies()` is invoked from the `taskCreated`, `taskUpdated`, and
|
|
37
|
+
`taskStatusUpdated` events. For `taskStatusUpdated` the call is **gated to terminal
|
|
38
|
+
statuses** (`isStatusComplete()`) so the dependent re-evaluation cascade only runs when a
|
|
39
|
+
task actually completes — not on every status change.
|
|
40
|
+
3. For the task, it builds `$waitingOn` — the list of dependencies (`$dependency->task_id ==
|
|
41
|
+
$taskId`) whose status is **not** complete. A dependency counts as complete when its status
|
|
42
|
+
`type` is one of `COMPLETE_STATUS_TYPES` (`done` **or** `closed`).
|
|
43
|
+
4. It also computes `$isStalledTask` (status in `on hold` / `awaiting client` / `roadblocked`).
|
|
44
|
+
5. Decision:
|
|
45
|
+
- `Committed` + (`$waitingOn` non-empty OR stalled) → set **Conditional** (+ comment).
|
|
46
|
+
- `Conditional` + `$waitingOn` empty + not stalled → set **Committed** (+ comment).
|
|
47
|
+
- `Stretch` with a due date inside the current sprint → set Committed/Conditional as above.
|
|
48
|
+
6. When a task is depended on by another (`$dependency->depends_on == $taskId`), it recurses
|
|
49
|
+
into the blocking direction so completing a task re-evaluates the tasks it was blocking. The
|
|
50
|
+
`$dependsOnTaskId` argument breaks two-node cycles.
|
|
51
|
+
|
|
52
|
+
## Data model
|
|
53
|
+
Reads/writes the `Team` database (`Tasks`, `Developers`, `Tasks_Developers`, `Sprints`) and the
|
|
54
|
+
ClickUp REST API. The Work Type itself lives in ClickUp as a custom field
|
|
55
|
+
(`CLICK_UP_CUSTOM_FIELD_ID__WORKTYPE`), not in the DB.
|
|
56
|
+
|
|
57
|
+
## Client variations
|
|
58
|
+
None — internal team/sprint tooling, uniform across clients.
|
|
59
|
+
|
|
60
|
+
## Gotchas / known issues
|
|
61
|
+
- **Two terminal status types.** ClickUp has both `done` and `closed` terminal status types. A
|
|
62
|
+
completed dependency may be `closed`, not `done`. The completion check must treat **both** as
|
|
63
|
+
finished — see `COMPLETE_STATUS_TYPES`. Checking only `== 'done'` leaves dependents stuck on
|
|
64
|
+
Conditional after their blocker is closed.
|
|
65
|
+
- **Status changes must trigger re-evaluation.** A dependency completing fires
|
|
66
|
+
`taskStatusUpdated` on the *dependency*, not the blocked task. The `taskStatusUpdated` case
|
|
67
|
+
must call `updateTaskAndDependencies()` (it recurses into dependents) or the auto-promotion
|
|
68
|
+
back to Committed never fires on its own.
|
|
69
|
+
- **No PHPUnit harness** in worker2. The regression test
|
|
70
|
+
`Tests/Worker/ClickupWorkTypeTest.php` is a plain-PHP script (reflection on
|
|
71
|
+
`isStatusComplete`) run with `php Tests/Worker/ClickupWorkTypeTest.php`.
|
|
72
|
+
|
|
73
|
+
## Change history
|
|
74
|
+
- 2026-06-23 — Fixed dependents staying Conditional after a blocker completed: completion check
|
|
75
|
+
now treats `closed` as terminal alongside `done`, and `taskStatusUpdated` re-evaluates
|
|
76
|
+
dependents (gated to terminal statuses). Added `COMPLETE_STATUS_TYPES`, `isStatusComplete()`,
|
|
77
|
+
and a plain-PHP regression test. (jcardinal)
|
|
78
|
+
|
|
79
|
+
## Related docs
|
|
80
|
+
- [ClickUp Project & Opportunity Multi-List Routing](./clickup-project-routing.md)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -15,13 +15,13 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
15
15
|
|
|
16
16
|
## 2.0 framework
|
|
17
17
|
|
|
18
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
19
|
-
- **worker2** (Worker) —
|
|
18
|
+
- **_underscore** (_Underscore) _(framework core)_ — 11 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
19
|
+
- **worker2** (Worker) — 10 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
20
20
|
- **api2** (API) — 4 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
21
21
|
- **dbchanges2** (Database Changes) _(framework core)_ — 1 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
22
22
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
23
23
|
- **saml** (SAML SSO Gateway) — 2 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
24
|
-
- **toga2-view** (TOGa View Frontend) —
|
|
24
|
+
- **toga2-view** (TOGa View Frontend) — 1 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
25
25
|
- **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
|
|
26
26
|
- **talos** (TOGa IQ) — 6 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
|
|
27
27
|
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
|
@@ -4,4 +4,5 @@
|
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [Rate Monthly Reconciliation Report](features/monthly-reconciliation-report.md) | 1.0 | A monthly cron that emails an Excel reconciliation report covering all Rate subscription sales orders and their linked PayPal payments for the prior calendar mo | worker/crons/notifications/reports/rate/send_monthly_rate_purchases_report.php, worker/schedules/cron.worker.notification.json |
|
|
6
6
|
| [Rate SAML SSO](features/saml-sso.md) | 2.0 | Rate uses Azure AD as its IdP (`login.rate.com`). | _underscore/Model/Rate/ClientAuthentication.php, saml/Controller/Index.php, toga2-view/src/hooks/useAuthenticationFlow.ts |
|
|
7
|
+
| [Service Card Entitlement Display](features/service-card-entitlements.md) | 2.0 | Rate's home and services pages display one service card per purchased entitlement. | src/components/ServiceCard/ServiceCard.tsx, src/components/ServiceCard/index.ts, src/hooks/useBundleServices.ts, src/pages/Home/api/homeApi.ts, src/pages/Home/view/HomePage.tsx, src/pages/Home/viewModels/useHomePageViewModel.ts, src/pages/Services/view/ServicesPage.tsx, src/pages/Services/viewModels/useServicePageViewModel.ts |
|
|
7
8
|
| [Rate](profile.md) | 2.0 | Rate is a mortgage/lending client. | |
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Service Card Entitlement Display"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga2-view
|
|
5
|
+
project: TOGa View
|
|
6
|
+
client: rate
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-23
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- src/components/ServiceCard/ServiceCard.tsx
|
|
13
|
+
- src/components/ServiceCard/index.ts
|
|
14
|
+
- src/hooks/useBundleServices.ts
|
|
15
|
+
- src/pages/Home/api/homeApi.ts
|
|
16
|
+
- src/pages/Home/view/HomePage.tsx
|
|
17
|
+
- src/pages/Home/viewModels/useHomePageViewModel.ts
|
|
18
|
+
- src/pages/Services/view/ServicesPage.tsx
|
|
19
|
+
- src/pages/Services/viewModels/useServicePageViewModel.ts
|
|
20
|
+
related:
|
|
21
|
+
- clients/rate/profile.md
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Summary
|
|
25
|
+
|
|
26
|
+
Rate's home and services pages display one service card per purchased entitlement. The card
|
|
27
|
+
shows a hero image, title, Active badge, contract number, plan, price (or address for warranty
|
|
28
|
+
cards), renewal date, and a Manage button. Previously the UI showed one card per bundle type
|
|
29
|
+
regardless of how many entitlements the user had purchased.
|
|
30
|
+
|
|
31
|
+
## Key files / entry points
|
|
32
|
+
|
|
33
|
+
- `ServiceCard.tsx` — the card component; accepts `serviceType` to switch the second data row
|
|
34
|
+
between Price (tech) and Address (warranty)
|
|
35
|
+
- `useBundleServices.ts` — shared hook returning one `BundleService` per active entitlement;
|
|
36
|
+
used by both `useHomePageViewModel` and `useServicePageViewModel`
|
|
37
|
+
- `homeApi.ts` — `getActiveServices()` fetches entitlements with subscription and address fields
|
|
38
|
+
- `HomePage.tsx` — shows max 3 cards; "View All Services" tertiary text button links to `/services`
|
|
39
|
+
- `ServicesPage.tsx` — shows all entitlements in a 2-column grid, scrollable
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
1. `getActiveServices(borrowerId)` queries `Entitlements` with:
|
|
44
|
+
- INNER JOIN: `Contacts`, `Customers`, `Subscriptions`
|
|
45
|
+
- LEFT JOIN (`ojoin`): `ContactAddresses`, `Addresses` — keeps entitlements whose contact
|
|
46
|
+
has no address instead of dropping the row
|
|
47
|
+
- Fields: `uuid`, `number`, `saleItem.title/uuid`, `subscription.isActive/amount/frequency/dateEnd`,
|
|
48
|
+
`contact.primaryContactAddress.address.line1`
|
|
49
|
+
|
|
50
|
+
2. `useBundleServices` filters `subscription.isActive === true` and builds one `BundleService`
|
|
51
|
+
per entitlement. It also fetches bundles to resolve `bundleUuid` for navigation.
|
|
52
|
+
|
|
53
|
+
3. `detectServiceType(title)` classifies each card:
|
|
54
|
+
- `'warranty'` — title includes "warranty" or "whole home" (and NOT "tech")
|
|
55
|
+
- `'tech'` — title includes "tech" or "support"
|
|
56
|
+
- `'other'` — everything else
|
|
57
|
+
|
|
58
|
+
4. `ServiceCard` renders the data rows based on `serviceType`:
|
|
59
|
+
- Tech / other → row 2: **Price** (`$XX.XX`)
|
|
60
|
+
- Warranty → row 2: **Address** (`contact.primaryContactAddress.address.line1`)
|
|
61
|
+
|
|
62
|
+
5. Home page slices `allServices` to 3 (`HOME_SERVICE_LIMIT`). If more exist, a tertiary
|
|
63
|
+
"View All Services" text link appears below the cards pointing to `/services`.
|
|
64
|
+
|
|
65
|
+
6. Services page renders all entitlements with no cap in a `grid-cols-1 md:grid-cols-2` grid.
|
|
66
|
+
|
|
67
|
+
## Data model
|
|
68
|
+
|
|
69
|
+
Address path in DB:
|
|
70
|
+
```
|
|
71
|
+
Entitlements.contactId
|
|
72
|
+
→ Contacts.primaryContactAddressId
|
|
73
|
+
→ ContactAddresses.id → ContactAddresses.addressId
|
|
74
|
+
→ Addresses.line1
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Contract number comes from `Entitlements.number` (auto-incremented with ET prefix, e.g. `ET100017`).
|
|
78
|
+
|
|
79
|
+
## Client variations
|
|
80
|
+
|
|
81
|
+
This feature is Rate-specific. The warranty card showing Address instead of Price is driven
|
|
82
|
+
by Rate's product mix (Whole Home Warranty + Tech Support). Other clients with different
|
|
83
|
+
product types would need `detectServiceType` extended or overridden.
|
|
84
|
+
|
|
85
|
+
## Gotchas / known issues
|
|
86
|
+
|
|
87
|
+
- **`ojoin` is required for address** — using INNER JOIN (`join`) drops every entitlement
|
|
88
|
+
whose contact has no `primaryContactAddressId`, silently hiding those cards.
|
|
89
|
+
- **`T00:00:00` in date formatting** — `formatDate` appends this before parsing to avoid
|
|
90
|
+
timezone offset shifting the renewal date back by one day.
|
|
91
|
+
- **Per-entitlement, not per-bundle** — `useBundleServices` intentionally does not dedup.
|
|
92
|
+
A user with 8 entitlements of the same product sees 8 cards. The old dedup-by-bundle
|
|
93
|
+
behavior (1 card per service type) was the previous design.
|
|
94
|
+
- **Inactive bundles are not shown** — the new design only renders active entitlements.
|
|
95
|
+
The "Add a subscription" dashed button (handled by `useAddSubscription` hook +
|
|
96
|
+
`AddSubscriptionSheet`) replaces the old "Get Started" inactive bundle cards.
|
|
97
|
+
- **`useBundleServices` is shared** — both `useHomePageViewModel` and `useServicePageViewModel`
|
|
98
|
+
call it. Changes to the hook affect both pages.
|
|
99
|
+
|
|
100
|
+
## Change history
|
|
101
|
+
|
|
102
|
+
- 2026-06-23 — Initial implementation: per-entitlement card display, new ServiceCard component,
|
|
103
|
+
address via LEFT JOIN, 3-card home limit with View All Services tertiary button (bala)
|
package/package.json
CHANGED