Outbound Group Management - Implementation Plan¶
Context¶
The TQPro admin module currently manages inbound tourism groups (travelers arriving to the UAE). The business now needs outbound group management: organizing trips for UAE-based travelers departing to international destinations. This involves a different workflow with supplier quote management, day-wise itinerary building, per-passenger visa tracking and invoicing, and tiered pricing.
Architecture decisions (confirmed): Shared database tables with groupType discriminator; separate frontend pages (outbound-groups-*.html).
Backward Compatibility Analysis¶
The outbound group feature shares database tables, JPA entities, canonical entities, facade, API class, frontend data models (groups-core.js), and PaxExcelService with the existing inbound group feature. All changes MUST be additive and must not alter existing inbound behavior.
Risk Assessment Summary¶
| Area | Risk | Mitigation |
|---|---|---|
| Inbound list shows outbound groups | HIGH | groups/group/list must default groupType='INBOUND' when param not provided |
| CTripDataMap new null arrays | HIGH | Initialize itinerary and pricingTiers as empty arrays [] in constructor, never null |
| PaxExcelService column order | HIGH | Only APPEND new columns (index 10+), never insert in the middle. parseRow() reads columns 0-9 by hardcoded index; row.getCell() returns null for missing columns so old files import safely |
| groups-core.js syntax break | MEDIUM | Test all inbound pages after modifying shared JS module |
| loadGroupExpanded response | LOW | New fields appear as additional JSON keys; inbound frontend ignores unknown keys. Arrays must never be null. |
| New DB columns on shared tables | LOW | All new columns are nullable with no default constraints; existing rows get NULL values |
| New field mappings in XML | LOW | EntityTransformer silently skips null source values (line 255 check); no errors on old data |
| New NTS services in nts-client.xml | LOW | Name-based lookup; new entries don't affect existing services |
| New fields on canonical entities | LOW | Plain POJOs; new null fields are harmless to existing code |
| New fields on JPA entities | LOW | Nullable @Column annotations; Hibernate handles gracefully |
| GroupManagerFacade new methods | LOW | Purely additive; existing methods unchanged |
| GroupApi new endpoints | LOW | New @Path entries don't affect existing endpoint routing |
Required Mitigations (must be implemented)¶
M1: Default groupType filter on existing list endpoint¶
File: GroupApi.java > doListGroups()
The existing groups/group/list endpoint currently returns ALL groups regardless of type. Once outbound groups exist in the shared tripgroup table, they would appear in the inbound group list page.
Fix: When groupType parameter is not provided, default to "INBOUND":
String groupType = ApiUtil.gmp(reqData, "groupType", String.class, false);
if (TypeUtil.isEmptyString(groupType)) {
groupType = "INBOUND"; // backward-compatible default
}
// add groupType to SelectCriteriaList
This ensures:
- Existing inbound frontend (groups-list.js) continues to see only inbound groups without any frontend changes
- New outbound frontend explicitly passes groupType="OUTBOUND" or uses the dedicated groups/outbound/list endpoint
M2: Initialize new CTripDataMap arrays as empty, never null¶
File: CTripDataMap.java
All new array fields must be initialized in the constructor to empty arrays:
private CGroupItinerary[] itinerary = new CGroupItinerary[0];
private CTripPricing[] pricingTiers = new CTripPricing[0];
In loadTripData(), always set these arrays even for inbound groups (set to empty arrays). This prevents any frontend code from encountering null when accessing these properties.
M3: PaxExcelService - append-only column additions¶
File: PaxExcelService.java
New columns (Emergency Contact, Emergency Tel, Visa Required, Visa Status) must be appended at indices 10-13, after the existing 10 columns (indices 0-9). The existing parseRow() reads columns 0-9 by hardcoded index; row.getCell(colIndex) returns null for non-existent columns, so:
- Old Excel files (10 columns) imported with new code: columns 10-13 return null, fields left empty. Safe.
- New Excel files (14 columns) imported with new code: all 14 columns parsed. Safe.
- New Excel files imported with old code (hypothetical rollback): columns 10-13 ignored. Safe.
M4: Inbound frontend smoke test after groups-core.js changes¶
After adding new classes (GroupItinerary, TripPricing), new API methods (OutboundGroupsAPI), and new constants (OUTBOUND_STATUS) to groups-core.js, all existing inbound group pages must be tested:
- groups-list.html - verify only inbound groups shown
- groups-summary.html - verify loads without JS errors
- groups-passengers.html - verify passenger CRUD works
- groups-accommodation.html - verify hotel CRUD works
- groups-rooming.html - verify room assignment works
- groups-activities.html - verify activity CRUD works
M5: Backfill existing data¶
File: outbound_groups.sql
After adding the grouptype column, backfill all existing rows:
ALTER TABLE nts.tripgroup ADD COLUMN grouptype VARCHAR(20) DEFAULT 'INBOUND';
UPDATE nts.tripgroup SET grouptype = 'INBOUND' WHERE grouptype IS NULL;
This ensures existing groups are explicitly marked as INBOUND and will be returned by the defaulting filter in M1.
Verified Safe (no mitigation needed)¶
These areas were analyzed and confirmed to be backward compatible without changes:
-
EntityTransformer null handling (
tqcommon/.../EntityTransformer.javaline 255): When a field mapping exists but the source value is null (as will happen for new columns on old data), the mapping is silently skipped. No errors, no data corruption. -
SelectCriteriaList-based queries (
GroupManagerFacade.getGroups()): Criteria are additive and optional. Adding a new criterion for groupType doesn't affect callers that don't provide it (with M1 default applied at API layer). -
Frontend model class update() methods (
groups-core.js): All classes use'fieldName' in objchecks. New properties from API responses that aren't referenced by inbound pages are silently ignored. -
FormBinder (
groups-core.js): Generic property-agnostic binding. New form fields withdata-entity-fieldattributes that don't exist in the HTML are simply not bound. -
JSON serialization of CTripDataMap: New fields appear as additional JSON keys. JavaScript object destructuring and property access simply returns
undefinedfor keys not referenced, which is safe.
Proposed Additional Features¶
| # | Feature | Justification |
|---|---|---|
| 1 | Group Status Workflow | DRAFT > QUOTED > NEGOTIATION > CONFIRMED > PREPARING > TRAVELING > COMPLETED / CANCELLED with color-coded badges |
| 2 | Financial Summary Dashboard | Total cost (supplier quotes) vs revenue (pricing tiers x pax), margin % on summary page |
| 3 | Passenger Emergency Contact | Required for international travel safety and insurance |
| 4 | Supplier Contact Info | Store supplier name + contact on each trip element for direct follow-up |
| 5 | Itinerary PDF Export | Printable day-by-day itinerary for passenger distribution |
| 6 | Payment Status Tracking | Show paid vs pending invoice status on invoicing page |
| 7 | Rooming List Copy Between Hotels | Copy room assignments when a group moves between hotels with similar setup |
Step 1: Database Schema¶
File: config/db-changes/outbound_groups.sql
ALTER existing tables¶
tripgroup - outbound discriminator + destination fields:
| Column | Type | Default | Purpose |
|---|---|---|---|
grouptype |
VARCHAR(20) | 'INBOUND' | Discriminator: INBOUND / OUTBOUND |
subtype |
VARCHAR(20) | NULL | COLLABORATIVE / SELF_ORGANIZED |
origin |
VARCHAR(200) | NULL | Origin city/airport |
destination |
VARCHAR(200) | NULL | Destination(s) |
trippax - visa + emergency + invoice fields:
| Column | Type | Purpose |
|---|---|---|
emergencycontact |
VARCHAR(200) | Emergency contact name |
emergencytel |
VARCHAR(50) | Emergency contact phone |
visarequired |
VARCHAR(20) | UNKNOWN / NOT_REQUIRED / VOA / ONLINE / REQUIRED |
visastatus |
VARCHAR(20) | DOC_PENDING / DOC_COMPLETE / APPLIED / APPROVED / REJECTED / NOT_NEEDED |
nationalitycode |
VARCHAR(3) | ISO Alpha-3 nationality code (for visa lookups) |
visaapplicationid |
INTEGER | FK to visa application (links pax to visa module) |
invoiceid |
INTEGER | FK to Odoo invoice (many pax -> one invoice) |
tripgroup - destination detail fields (TQ-58):
| Column | Type | Purpose |
|---|---|---|
destinationcountry |
VARCHAR(3) | Destination country ISO Alpha-3 code |
destinationcity |
VARCHAR(200) | Destination city name |
triphotel, tripservice, grpxport - supplier fields (same 5 columns on each):
| Column | Type | Purpose |
|---|---|---|
suppliername |
VARCHAR(200) | Supplier company name |
suppliercontact |
VARCHAR(200) | Supplier contact info |
quotedocpath |
VARCHAR(500) | Path to uploaded quote document |
inclusions |
VARCHAR(2000) | What's included |
exclusions |
VARCHAR(2000) | What's excluded |
Also widen triphotel.hoteldesc from VARCHAR(50) to VARCHAR(500) for free-text hotel names.
CREATE new tables¶
nts.group_itinerary - day-wise itinerary:
| Column | Type | Constraint |
|---|---|---|
id |
INTEGER | PK, sequence nts.group_itinerary_seq |
groupid |
INTEGER | FK to tripgroup, NOT NULL |
daynum |
INTEGER | NOT NULL |
daydate |
DATE | |
daytitle |
VARCHAR(200) | NOT NULL |
daydesc |
TEXT |
nts.trippricing - per-person pricing tiers:
| Column | Type | Constraint |
|---|---|---|
trippricingid |
INTEGER | PK, sequence nts.trippricing_seq |
groupid |
INTEGER | FK to tripgroup, NOT NULL |
basisdesc |
VARCHAR(200) | NOT NULL (e.g. "Adult", "Child 4-11") |
priceamount |
DOUBLE PRECISION | NOT NULL, DEFAULT 0 |
numpax |
INTEGER | NOT NULL, DEFAULT 0 |
currencycode |
VARCHAR(3) |
Backfill: UPDATE nts.tripgroup SET grouptype = 'INBOUND' WHERE grouptype IS NULL;
Step 2: Extend Existing JPA Entities¶
Add @Column-annotated fields + getters/setters, following the existing pattern in each file.
| Entity File | New Fields |
|---|---|
tqapp/.../client/nts/db/group/TripgroupEntity.java |
grouptype, subtype, origin, destination, destinationcountry, destinationcity |
tqapp/.../client/nts/db/group/TrippaxEntity.java |
emergencycontact, emergencytel, visarequired, visastatus, nationalitycode, visaapplicationid, invoiceid |
tqapp/.../client/nts/db/group/TriphotelEntity.java |
suppliername, suppliercontact, quotedocpath, inclusions, exclusions; widen hoteldesc @Size to 500 |
tqapp/.../client/nts/db/group/TripserviceEntity.java |
suppliername, suppliercontact, quotedocpath, inclusions, exclusions |
tqapp/.../client/nts/db/group/GrpxportEntity.java |
suppliername, suppliercontact, quotedocpath, inclusions, exclusions |
Step 3: Create New JPA Entities¶
tqapp/src/main/java/com/perun/tlinq/client/nts/db/group/GroupItineraryEntity.java
- Extends NTSEntity, @Table(name="group_itinerary", schema="nts")
- PK: id with @SequenceGenerator(sequenceName="nts.group_itinerary_seq")
- Fields: groupid (Integer), daynum (Integer), daydate (Date, @Temporal(DATE)), daytitle (String, max 200), daydesc (String)
tqapp/src/main/java/com/perun/tlinq/client/nts/db/group/TrippricingEntity.java
- Extends NTSEntity, @Table(name="trippricing", schema="nts")
- PK: id with @SequenceGenerator(sequenceName="nts.trippricing_seq")
- Fields: groupid (Integer), basisdesc (String, max 200), priceamount (Double), numpax (Integer), currencycode (String, max 3)
Step 4: Extend Existing Canonical Entities¶
Add private fields + getters/setters (no annotations, plain POJOs).
| Entity File | New Fields |
|---|---|
tqapp/.../entity/group/CTripGroup.java |
groupType (String), subType (String), origin (String), destination (String), destinationCountry (String), destinationCity (String) |
tqapp/.../entity/group/CTripPax.java |
emergencyContact (String), emergencyTel (String), visaRequired (String), visaStatus (String), nationalityCode (String), visaApplicationId (Integer), invoiceId (Integer) |
tqapp/.../entity/group/CTripHotel.java |
supplierName (String), supplierContact (String), quoteDocPath (String), inclusions (String), exclusions (String) |
tqapp/.../entity/group/CTripService.java |
supplierName (String), supplierContact (String), quoteDocPath (String), inclusions (String), exclusions (String) |
tqapp/.../entity/group/CTripTransport.java |
supplierName (String), supplierContact (String), quoteDocPath (String), inclusions (String), exclusions (String) |
Step 5: Create New Canonical Entities¶
tqapp/src/main/java/com/perun/tlinq/entity/group/CGroupItinerary.java
- Extends TlinqEntity implements Serializable
- Fields: groupItineraryId (Integer), groupId (Integer), dayNum (Integer), dayDate (Date), dayTitle (String), dayDesc (String)
tqapp/src/main/java/com/perun/tlinq/entity/group/CTripPricing.java
- Extends TlinqEntity implements Serializable
- Fields: tripPricingId (Integer), groupId (Integer), basisDesc (String), priceAmount (Double), numPax (Integer), currencyCode (String)
tqapp/src/main/java/com/perun/tlinq/entity/group/CTripFinancialSummary.java (DTO, not a persisted entity)
- Fields: totalCost (Double), totalRevenue (Double), margin (Double), marginPercent (Double), currencyCode (String), costBreakdown (Map: "HOTEL"/"ACTIVITY"/"TRANSPORT" -> Double)
Step 6: Extend CTripDataMap¶
File: tqapp/src/main/java/com/perun/tlinq/entity/group/CTripDataMap.java
Add:
- CGroupItinerary[] itinerary + HashMap<Integer, CGroupItinerary> itineraryMap (keyed by dayNum)
- CTripPricing[] pricingTiers + HashMap<Integer, CTripPricing> pricingMap (keyed by tripPricingId)
- Getters/setters following existing fluent builder pattern
Update GroupManagerFacade.loadTripData() to populate these when groupType == "OUTBOUND".
Step 7: NTS Service Config¶
File: config/nts-client.xml (add in Group Management section)
| Service Name | Class | Method | Entity |
|---|---|---|---|
saveGroupItinerary |
NTSEntityWriteService |
(default) | GroupItineraryEntity |
readGroupItinerary |
NTSEntityReadService |
(default) | GroupItineraryEntity |
deleteGroupItinerary |
NTSGroupService |
deleteGroupItinerary |
GroupItineraryEntity |
saveTripPricing |
NTSEntityWriteService |
(default) | TrippricingEntity |
readTripPricing |
NTSEntityReadService |
(default) | TrippricingEntity |
deleteTripPricing |
NTSGroupService |
deleteTripPricing |
TrippricingEntity |
copyRoomingList |
NTSGroupService |
copyRoomingList |
RoomlistEntity |
Add corresponding methods to NTSGroupService.java:
- deleteGroupItinerary(RemoteEntityI) - delete by named param groupItineraryId
- deleteTripPricing(RemoteEntityI) - delete by named param tripPricingId
- copyRoomingList(RemoteEntityI) - load rooms from source hotel, clone to target hotel, copy roompax assignments
Step 8: Entity Mapping Config¶
File: config/entities/group-entities.xml
Extend existing mappings (add DirectMapping FieldMappings)¶
- TripGroup:
groupType<->grouptype,subType<->subtype,origin<->origin,destination<->destination,destinationCountry<->destinationcountry,destinationCity<->destinationcity - TripPax:
emergencyContact<->emergencycontact,emergencyTel<->emergencytel,visaRequired<->visarequired,visaStatus<->visastatus,nationalityCode<->nationalitycode,visaApplicationId<->visaapplicationid,invoiceId<->invoiceid - TripHotel:
supplierName<->suppliername,supplierContact<->suppliercontact,quoteDocPath<->quotedocpath,inclusions<->inclusions,exclusions<->exclusions - TripService: same 5 supplier fields
- TripTransport: same 5 supplier fields
Add new entity definitions¶
GroupItinerary entity:
- Services: saveGroupItinerary (create/update), readGroupItinerary (read/search), deleteGroupItinerary (delete)
- All fields use DirectMapping: groupItineraryId<->id, groupId<->groupid, dayNum<->daynum, dayDate<->daydate, dayTitle<->daytitle, dayDesc<->daydesc
TripPricing entity:
- Services: saveTripPricing (create/update), readTripPricing (read/search), deleteTripPricing (delete)
- All fields use DirectMapping: tripPricingId<->id, groupId<->groupid, basisDesc<->basisdesc, priceAmount<->priceamount, numPax<->numpax, currencyCode<->currencycode
Step 9: Extend GroupManagerFacade¶
File: tqapp/src/main/java/com/perun/tlinq/entity/group/GroupManagerFacade.java
New methods:
| Method | Purpose |
|---|---|
getOutboundGroups(SelectCriteriaList) |
Add groupType=OUTBOUND criterion, delegate to getGroups() |
getItinerary(SelectCriteriaList) |
search(FACTORY, "GroupItinerary", scl) |
deleteItineraryDay(Integer) |
Custom service delete by ID |
getPricingTiers(SelectCriteriaList) |
search(FACTORY, "TripPricing", scl) |
deletePricingTier(Integer) |
Custom service delete by ID |
checkVisaRequirements(Integer groupId, String destination) |
For each pax: if nationality matches destination, short-circuit to NOT_REQUIRED. Otherwise convert codes to Alpha-2 and call VisaRequirementService.lookupRequirement(). Classify response using classifyVisaCategory() — a data-driven rules array matching visa_rules.primary_rule.name keywords (with fallback to legacy category field). Categories: NOT_REQUIRED, VOA, ONLINE (includes eTA), REQUIRED, UNKNOWN. |
createVisaApplicationForPax(Integer paxId, String callerUser) |
Creates CRM customer from pax name/phone/email, creates visa application and applicant, links pax via visaApplicationId, sets visaStatus to DOC_PENDING. |
calculateFinancialSummary(Integer groupId) |
Sum hotel/service/transport costs; sum pricing tier priceAmount * numPax for revenue; return CTripFinancialSummary |
copyRoomingList(Integer groupId, Integer srcHotelId, Integer tgtHotelId) |
Delegate to NTSGroupService custom service |
createPaxInvoice(Integer groupId, List<Integer> paxIds, Double totalPrice, String description) |
Create Odoo quote via QuotationFacade.create() with single "Group Trip" line item (qty=1, price=totalPrice, description includes pax breakdown), confirm and invoice, set invoiceId on each pax |
Also extend loadTripData() to populate itinerary and pricing arrays when groupType == "OUTBOUND".
Step 10: Extend GroupApi¶
File: tqapi/src/main/java/com/perun/tlinq/api/GroupApi.java
Modify existing¶
groups/group/list- accept optionalgroupTypeparam to filter
New endpoints (all POST, follow existing pattern)¶
| Endpoint | Purpose | Key Params |
|---|---|---|
groups/outbound/list |
List outbound groups | departureFrom, departureTo, subType? |
groups/itinerary/list |
List itinerary days | groupId |
groups/itinerary/write |
Create/update day | CGroupItinerary fields |
groups/itinerary/delete |
Delete day | groupItineraryId |
groups/pricing/list |
List pricing tiers | groupId |
groups/pricing/write |
Create/update tier | CTripPricing fields |
groups/pricing/delete |
Delete tier | tripPricingId |
groups/financial/summary |
Financial cost/revenue | groupId |
groups/pax/checkVisa |
Bulk visa check | groupId, destination (Alpha-2 or Alpha-3) |
groups/pax/createVisaApp |
Create visa application for pax | paxId |
groups/pax/exportExcel |
Export pax to Excel | groupId |
groups/rooming/copy |
Copy rooming between hotels | groupId, sourceHotelId, targetHotelId |
groups/pax/createInvoice |
Invoice for pax group | groupId, paxIds[], totalPrice, description |
groups/tripservice/list |
List services | groupId |
groups/tripservice/delete |
Delete service | tripServiceId |
Each endpoint follows the existing pattern: extract session, extract params via ApiUtil.gmp(), delegate to GroupManagerFacade via a private doXXX() method, wrap result in TlinqApiResponse.
Step 11: API Roles¶
File: config/api-roles.properties - add all new endpoints with agent,admin access:
# Outbound Group Management
groups/outbound/list=agent,admin
groups/itinerary/list=agent,admin
groups/itinerary/write=agent,admin
groups/itinerary/delete=agent,admin
groups/pricing/list=agent,admin
groups/pricing/write=agent,admin
groups/pricing/delete=agent,admin
groups/financial/summary=agent,admin
groups/pax/checkVisa=agent,admin
groups/pax/createVisaApp=agent,admin
groups/pax/exportExcel=agent,admin
groups/rooming/copy=agent,admin
groups/pax/createInvoice=agent,admin
groups/tripservice/list=agent,admin
groups/tripservice/delete=agent,admin
Step 12: Frontend Pages¶
All in tqweb-adm/. Bootstrap 5, ES6 modules. Follow existing groups-* page patterns from the inbound group management.
Extend shared module¶
tqweb-adm/js/modules/groups-core.js - add:
- GroupItinerary class (groupItineraryId, groupId, dayNum, dayDate, dayTitle, dayDesc)
- TripPricing class (tripPricingId, groupId, basisDesc, priceAmount, numPax, currencyCode)
- OUTBOUND_STATUS constants: DRAFT, QUOTED, NEGOTIATION, CONFIRMED, PREPARING, TRAVELING, COMPLETED, CANCELLED
- OutboundGroupsAPI object with all new endpoint calls
- getOutboundStatusBadgeClass() utility with distinct colors per status
New pages (10 pages = 10 HTML + 10 JS modules)¶
| Page | Files | Key Features |
|---|---|---|
| Group List | outbound-groups-list.html + .js |
Filterable table, status badges, create modal (name, partner, type, origin, destination, dates, numPax) |
| Summary | outbound-groups-summary.html + .js |
Header card, status workflow, financial dashboard (cost/revenue/margin), nav tabs to sub-pages |
| Itinerary | outbound-groups-itinerary.html + .js |
Day cards sortable by dayNum, auto-generate days from date range, add/edit/delete, PDF export button |
| Passengers | outbound-groups-passengers.html + .js |
Table with visa badges, "Check Visa" bulk button, Excel import/export (with visa columns), filter "needs visa" toggle, emergency contact fields |
| Accommodation | outbound-groups-accommodation.html + .js |
Cards per hotel (free-text name, no product lookup), supplier info, quote upload, inclusions/exclusions, cost/price, "Copy Rooming" button |
| Activities | outbound-groups-activities.html + .js |
Cards/table (free-text name, no product lookup), supplier info, quote upload, inclusions/exclusions, cost |
| Transport | outbound-groups-transport.html + .js |
Transport entries with supplier info, quote upload, transfer type (origin/destination/inter-city) |
| Rooming | outbound-groups-rooming.html + .js |
Hotel selector dropdown, room cards with pax assignment, "Copy from hotel" feature, capacity indicators |
| Invoicing | outbound-groups-invoicing.html + .js |
Pax table with invoice status (none/quoted/invoiced/paid), select pax for invoice, create invoice (qty=1, "Group Trip" item), print invoice button |
| Pricing | outbound-groups-pricing.html + .js |
Pricing tier table (basis, amount, count), add/edit/delete, live total revenue calculation |
Step 13: Extend PaxExcelService¶
File: tqapp/src/main/java/com/perun/tlinq/entity/group/PaxExcelService.java
- Add 4 columns to template headers: "Emergency Contact", "Emergency Tel", "Visa Required", "Visa Status"
- Update
FIELD_NAMESmapping for the new columns - Update
parseRow()to handle columns 10-13 - Add
generateExport(List<CTripPax>)method returning filled XLSX byte[] (used by/pax/exportExcel)
Step 14: Testing¶
| Test File | Scope |
|---|---|
tqapp/src/test/.../nts/db/group/GroupItineraryEntityTest.java |
JPA CRUD for itinerary entity |
tqapp/src/test/.../nts/db/group/TrippricingEntityTest.java |
JPA CRUD for pricing entity |
tqapp/src/test/.../entity/group/GroupManagerFacadeOutboundTest.java |
Facade methods: outbound CRUD, visa check, financial summary, rooming copy |
tqapi/src/test/.../api/GroupApiOutboundTest.java |
API endpoint smoke tests (HTTP 200, response structure) |
tqapp/src/test/.../entity/group/PaxExcelServiceTest.java |
Template generation + export with new columns |
All tests use @BeforeAll to set TLINQ_HOME to config/ directory, following existing test patterns.
Step 15: Navigation¶
File: tqweb-adm/header_bootstrap.html
Update the "Outbound" nav item to link to outbound-groups-list.html.
Verification¶
- Run
outbound_groups.sqlagainst DB; verify columns exist, existing data hasgrouptype='INBOUND' ./gradlew build- all modules compile./gradlew :tqapp:test :tqapi:test- all tests pass- Start server in dev-mode, POST
groups/outbound/list- returns empty array - Create outbound group via API, verify all fields persist
- Add itinerary days, pricing tiers; verify
groups/financial/summary - Add passengers, run
groups/pax/checkVisa- verify visa fields updated - Create accommodation, add rooms, copy rooming to second hotel
- Create passenger invoice via
groups/pax/createInvoice - Navigate to
outbound-groups-list.htmlin browser, walk through all sub-pages
Implementation Order¶
Given the dependencies, implement in this order:
- Steps 1-3 (DB schema + JPA entities) - foundation layer
- Steps 4-5 (canonical entities + DTO) - business objects
- Steps 6-8 (CTripDataMap + NTS services + entity config) - wiring layer
- Step 9 (facade) - business logic
- Steps 10-11 (API endpoints + roles) - expose to frontend
- Step 13 (PaxExcelService) - Excel support
- Step 14 (tests) - verify backend
- Steps 12, 15 (frontend pages + navigation) - UI layer
File Summary¶
Files to Create (31 files)¶
| File | Type |
|---|---|
config/db-changes/outbound_groups.sql |
SQL schema (initial outbound setup) |
config/db-changes/0024-visa-pax-enhancements.sql |
SQL schema (TQ-58: destinationcountry, destinationcity, nationalitycode, visaapplicationid) |
tqapp/.../client/nts/db/group/GroupItineraryEntity.java |
JPA Entity |
tqapp/.../client/nts/db/group/TrippricingEntity.java |
JPA Entity |
tqapp/.../entity/group/CGroupItinerary.java |
Canonical Entity |
tqapp/.../entity/group/CTripPricing.java |
Canonical Entity |
tqapp/.../entity/group/CTripFinancialSummary.java |
DTO |
tqapp/src/test/.../GroupItineraryEntityTest.java |
Test |
tqapp/src/test/.../TrippricingEntityTest.java |
Test |
tqapp/src/test/.../GroupManagerFacadeOutboundTest.java |
Test |
tqapp/src/test/.../PaxExcelServiceTest.java |
Test |
tqapi/src/test/.../GroupApiOutboundTest.java |
Test |
tqweb-adm/outbound-groups-list.html |
Frontend |
tqweb-adm/outbound-groups-summary.html |
Frontend |
tqweb-adm/outbound-groups-itinerary.html |
Frontend |
tqweb-adm/outbound-groups-passengers.html |
Frontend |
tqweb-adm/outbound-groups-accommodation.html |
Frontend |
tqweb-adm/outbound-groups-activities.html |
Frontend |
tqweb-adm/outbound-groups-transport.html |
Frontend |
tqweb-adm/outbound-groups-rooming.html |
Frontend |
tqweb-adm/outbound-groups-invoicing.html |
Frontend |
tqweb-adm/outbound-groups-pricing.html |
Frontend |
tqweb-adm/js/modules/outbound-groups-list.js |
JS Module |
tqweb-adm/js/modules/outbound-groups-summary.js |
JS Module |
tqweb-adm/js/modules/outbound-groups-itinerary.js |
JS Module |
tqweb-adm/js/modules/outbound-groups-passengers.js |
JS Module |
tqweb-adm/js/modules/outbound-groups-accommodation.js |
JS Module |
tqweb-adm/js/modules/outbound-groups-activities.js |
JS Module |
tqweb-adm/js/modules/outbound-groups-transport.js |
JS Module |
tqweb-adm/js/modules/outbound-groups-rooming.js |
JS Module |
tqweb-adm/js/modules/outbound-groups-invoicing.js |
JS Module |
tqweb-adm/js/modules/outbound-groups-pricing.js |
JS Module |
Files to Modify (20 files)¶
| File | Changes |
|---|---|
tqapp/.../client/nts/db/group/TripgroupEntity.java |
+4 fields |
tqapp/.../client/nts/db/group/TrippaxEntity.java |
+5 fields |
tqapp/.../client/nts/db/group/TriphotelEntity.java |
+5 fields, widen hoteldesc |
tqapp/.../client/nts/db/group/TripserviceEntity.java |
+5 fields |
tqapp/.../client/nts/db/group/GrpxportEntity.java |
+5 fields |
tqapp/.../entity/group/CTripGroup.java |
+4 fields |
tqapp/.../entity/group/CTripPax.java |
+5 fields |
tqapp/.../entity/group/CTripHotel.java |
+5 fields |
tqapp/.../entity/group/CTripService.java |
+5 fields |
tqapp/.../entity/group/CTripTransport.java |
+5 fields |
tqapp/.../entity/group/CTripDataMap.java |
+2 arrays, +2 maps |
tqapp/.../entity/group/GroupManagerFacade.java |
+9 methods, extend loadTripData |
tqapp/.../entity/group/PaxExcelService.java |
+4 columns, +generateExport() |
tqapp/.../client/nts/service/group/NTSGroupService.java |
+3 methods |
tqapi/.../api/GroupApi.java |
+14 endpoints, modify 1 |
config/entities/group-entities.xml |
+24 field mappings, +2 entity defs |
config/nts-client.xml |
+7 service entries |
config/api-roles.properties |
+14 role entries |
tqweb-adm/js/modules/groups-core.js |
+2 classes, +API object, +status utils |
tqweb-adm/header_bootstrap.html |
Update Outbound nav link |