Skip to content

Commit d28934e

Browse files
authored
Improve address creation error messages (#194)
* Improve address creation error messages * Update gitignore * Remove tmp files * Lint * Also improve error messaging for create location (not bulk) * Also improve error messaging for update location (not bulk) * Remove search from business documents * Lint * update more error messages
1 parent 4d38fb0 commit d28934e

File tree

9 files changed

+84
-83
lines changed

9 files changed

+84
-83
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
.idea/
33
log
44
.DS_Store
5+
backend/tmp/
56

67
.vscode/

backend/src/modules/location-address/service.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,15 @@ export class LocationAddressService implements ILocationAddressService {
8080
createLocationAddress = withServiceErrorHandling(
8181
async (payload: CreateLocationAddressDTO, companyId: string): Promise<CreateLocationAddressResponse> => {
8282
const fipsLocation = await this.locationMatcher.getLocationFips(payload);
83-
84-
if (fipsLocation === null) {
85-
throw Boom.badRequest("Fips state and county code cannot be null");
83+
if (
84+
fipsLocation === null ||
85+
!fipsLocation ||
86+
fipsLocation.fipsStateCode === null ||
87+
fipsLocation.fipsCountyCode === null
88+
) {
89+
throw Boom.badRequest(
90+
`Please enter a valid address. Unable to validate address: ${payload.streetAddress}, ${payload.city}, ${payload.stateProvince}.`
91+
);
8692
}
8793

8894
// Add FIPS codes into payload to send to transaction layer
@@ -137,6 +143,11 @@ export class LocationAddressService implements ILocationAddressService {
137143
const transformedPayload = await Promise.all(
138144
payload.map(async (element) => {
139145
const currFips = await this.locationMatcher.getLocationFips(element);
146+
if (!currFips || currFips.fipsStateCode === null || currFips.fipsCountyCode === null) {
147+
throw Boom.badRequest(
148+
`Please enter a valid address. Unable to validate address: ${element.streetAddress}, ${element.city}, ${element.stateProvince}.`
149+
);
150+
}
140151
return { ...element, ...currFips };
141152
})
142153
);
@@ -177,8 +188,15 @@ export class LocationAddressService implements ILocationAddressService {
177188
// get the new fips codes if any of the address fields have changed
178189
const fipsLocation = await this.locationMatcher.getLocationFips(locationForMatching);
179190

180-
if (fipsLocation === null) {
181-
throw Boom.badRequest("Fips state and county code cannot be null");
191+
if (
192+
fipsLocation === null ||
193+
!fipsLocation ||
194+
fipsLocation.fipsStateCode === null ||
195+
fipsLocation.fipsCountyCode === null
196+
) {
197+
throw Boom.badRequest(
198+
`Please enter a valid address. Unable to validate address: ${payload.streetAddress}, ${payload.city}, ${payload.stateProvince}.`
199+
);
182200
}
183201

184202
const updatedLocationWithFips = {

backend/src/types/Location.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const LocationAddressSchema = z.object({
1111
city: z.string(),
1212
streetAddress: z.string(),
1313
postalCode: z.string().nonempty().regex(/^\d+$/, {
14-
message: "Must be a non-negative number string",
14+
message: "Please enter a valid Postal Code. Must be a non-negative number.",
1515
}),
1616
county: z.string().optional(),
1717
companyId: z.uuid(),
@@ -29,7 +29,7 @@ export const LocationAddressSchemaType = z.object({
2929
city: z.string(),
3030
streetAddress: z.string(),
3131
postalCode: z.string().nonempty().regex(/^\d+$/, {
32-
message: "Must be a non-negative number string",
32+
message: "Please enter a valid Postal Code. Must be a non-negative number.",
3333
}),
3434
county: z.string().optional(),
3535
companyId: z.uuid(),
@@ -45,7 +45,7 @@ export const CreateLocationAddressSchema = z.object({
4545
city: z.string().nonempty(),
4646
streetAddress: z.string().nonempty(),
4747
postalCode: z.string().nonempty().regex(/^\d+$/, {
48-
message: "Must be a non-negative number string",
48+
message: "Please enter a valid Postal Code. Must be a non-negative number.",
4949
}),
5050
county: z.string().nonempty().optional(),
5151
});
@@ -72,7 +72,7 @@ export const GetAllLocationAddressesSchema = z.array(
7272
city: z.string().nonempty(),
7373
streetAddress: z.string().nonempty(),
7474
postalCode: z.string().nonempty().regex(/^\d+$/, {
75-
message: "Must be a non-negative number string",
75+
message: "Please enter a valid Postal Code. Must be a non-negative number.",
7676
}),
7777
county: z.string().nonempty().optional(),
7878
companyId: z.uuid(),
@@ -93,7 +93,7 @@ export const UpdateLocationAddressDTOSchema = z.object({
9393
postalCode: z
9494
.string()
9595
.regex(/^\d+$/, {
96-
message: "Must be a non-negative number string",
96+
message: "Please enter a valid Postal Code. Must be a non-negative number.",
9797
})
9898
.optional(),
9999
county: z.string().optional().nullable(),

backend/src/utilities/error.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const withServiceErrorHandling = <T extends any[], R>(handler: (...args:
2424
case "23502":
2525
throw Boom.badRequest("Missing required field");
2626
default:
27-
throw Boom.internal(error, { message: "An expected error occured" });
27+
throw Boom.internal(error, { message: "An unexpected error occured" });
2828
}
2929
} else {
3030
throw Boom.boomify(error);

frontend/api/location.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ export const createLocation = async (payload: CreateLocationRequest): Promise<Lo
2020
if (response.ok) {
2121
return data!;
2222
} else {
23-
throw Error(error?.error);
23+
const apiError = new Error(error?.error || "Failed to create locations - Unkown Error") as Error & {
24+
status: number;
25+
statusText: string;
26+
};
27+
apiError.status = response.status;
28+
apiError.statusText = response.statusText;
29+
throw apiError;
2430
}
2531
};
2632
return authWrapper<Location>()(req);
@@ -36,7 +42,13 @@ export const createLocationBulk = async (payload: CreateLocationBulkRequest): Pr
3642
if (response.ok) {
3743
return data!;
3844
} else {
39-
throw Error(error?.error);
45+
const apiError = new Error(error?.error || "Failed to create locations - Unkown Error") as Error & {
46+
status: number;
47+
statusText: string;
48+
};
49+
apiError.status = response.status;
50+
apiError.statusText = response.statusText;
51+
throw apiError;
4052
}
4153
};
4254
return authWrapper<Location[]>()(req);

frontend/app/business-profile/overview/InsuranceCard.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export default function InsuranceCard() {
3434
setEditingInsuranceIndex(null);
3535
},
3636
onError: (error: Error) => {
37-
setSaveError("An error occurred while saving the location: " + error.message);
37+
const errorMessage = error.message || "Error updating policy. Check required fields and try again";
38+
setSaveError(errorMessage);
3839
},
3940
});
4041

@@ -45,7 +46,8 @@ export default function InsuranceCard() {
4546
setEditingInsuranceIndex(null);
4647
},
4748
onError: (error: Error) => {
48-
setSaveError("An error occurred while saving the location: " + error.message);
49+
const errorMessage = error.message || "Error creating policy. Check required fields and try again";
50+
setSaveError(errorMessage);
4951
},
5052
});
5153

frontend/app/business-profile/overview/LocationsCard.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export default function LocationsCard() {
3131
if (error.message.includes("postalCode")) {
3232
setSaveError("Error updating location. Please check postal code details and try again.");
3333
} else {
34-
setSaveError("An error occurred while saving the location.");
34+
const errorMessage = error.message || "Error updating location. Check required fields and try again";
35+
setSaveError(errorMessage);
3536
}
3637
},
3738
});
@@ -46,7 +47,8 @@ export default function LocationsCard() {
4647
if (error.message.includes("postalCode")) {
4748
setSaveError("Error creating location. Please check postal code details and try again.");
4849
} else {
49-
setSaveError("An error occurred while creating the location.");
50+
const errorMessage = error.message || "Error creating location. Check required fields and try again";
51+
setSaveError(errorMessage);
5052
}
5153
},
5254
});
@@ -58,7 +60,8 @@ export default function LocationsCard() {
5860
setEditingLocationIndex(null);
5961
},
6062
onError: (_error: Error) => {
61-
setSaveError("An error occurred while deleting the location.");
63+
const errorMessage = _error.message || "Error removing location. Check required fields and try again";
64+
setSaveError(errorMessage);
6265
},
6366
});
6467

frontend/app/signup/company.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ export default function Company({ handleNext: incrementNext }: CompanyInfoProps)
7373
incrementNext();
7474
},
7575
onError: (_error: Error) => {
76-
setLocError("Error creating locations. Check required fields and try again");
76+
const errorMessage = _error.message || "Error creating locations. Check required fields and try again";
77+
setLocError(errorMessage);
7778
},
7879
});
7980

frontend/components/table/CategorySelector.tsx

Lines changed: 28 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export default function CategorySelector({
2424
categories = defaultCategories,
2525
}: CategorySelectorProps) {
2626
const [isOpen, setIsOpen] = useState(false);
27-
const [searchQuery, setSearchQuery] = useState("");
2827
const dropdownRef = useRef<HTMLDivElement>(null);
2928
const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(null);
3029
const portalDropdownRef = useRef<HTMLDivElement>(null);
@@ -62,12 +61,9 @@ export default function CategorySelector({
6261
};
6362
}, [isOpen]);
6463

65-
const filteredCategories = categories.filter((cat) => cat.toLowerCase().includes(searchQuery.toLowerCase()));
66-
6764
const handleCategorySelect = (category: string) => {
6865
onCategoryChange(category);
6966
setIsOpen(false);
70-
setSearchQuery("");
7167
};
7268

7369
const handleRemoveCategory = (e: React.MouseEvent) => {
@@ -107,78 +103,46 @@ export default function CategorySelector({
107103
createPortal(
108104
<div
109105
ref={portalDropdownRef}
110-
className=" top-full left-0 right-0 mt-2 bg-white rounded-lg shadow-lg border border-gray-200 z-[999999] max-h-[400px] overflow-hidden"
106+
className="top-full left-0 right-0 mt-2 bg-white rounded-lg shadow-lg border border-gray-200 z-[999999] max-h-[400px] overflow-hidden"
111107
style={{
112108
position: "absolute",
113109
top: coords.top,
114110
left: coords.left,
115111
width: coords.width,
116112
}}
117113
>
118-
{/* Search Input */}
119-
<div className="p-4 border-b border-gray-200">
120-
<input
121-
type="text"
122-
placeholder="Select an option or create one"
123-
value={searchQuery}
124-
onChange={(e) => setSearchQuery(e.target.value)}
125-
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500 text-sm"
126-
autoFocus
127-
/>
128-
</div>
129-
130114
{/* Category List */}
131115
<div className="overflow-y-auto max-h-[300px]">
132-
{filteredCategories.length > 0 ? (
133-
filteredCategories.map((category) => (
134-
<div
135-
key={category}
136-
onClick={() => handleCategorySelect(category)}
137-
className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors"
138-
>
139-
{/* Drag Handle */}
140-
<div className="flex flex-col gap-[2px]">
141-
<div className="flex gap-[2px]">
142-
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
143-
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
144-
</div>
145-
<div className="flex gap-[2px]">
146-
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
147-
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
148-
</div>
149-
<div className="flex gap-[2px]">
150-
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
151-
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
152-
</div>
116+
{categories.map((category) => (
117+
<div
118+
key={category}
119+
onClick={() => handleCategorySelect(category)}
120+
className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors"
121+
>
122+
{/* Drag Handle */}
123+
<div className="flex flex-col gap-[2px]">
124+
<div className="flex gap-[2px]">
125+
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
126+
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
153127
</div>
154-
155-
{/* Category */}
156-
<span
157-
className={`px-3 py-1 rounded-md text-sm font-medium ${defaultCategoryColors[category] || "bg-gray-100 text-gray-700"}`}
158-
>
159-
{category}
160-
</span>
161-
</div>
162-
))
163-
) : (
164-
<div className="px-4 py-8 text-center text-gray-500 text-sm">
165-
{searchQuery ? (
166-
<div>
167-
<p>No matching categories</p>
168-
<button
169-
onClick={() => {
170-
handleCategorySelect(searchQuery);
171-
}}
172-
className="mt-2 text-purple-600 hover:text-purple-700 font-medium"
173-
>
174-
Create &quot;{searchQuery}&quot;
175-
</button>
128+
<div className="flex gap-[2px]">
129+
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
130+
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
131+
</div>
132+
<div className="flex gap-[2px]">
133+
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
134+
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
176135
</div>
177-
) : (
178-
"No categories available"
179-
)}
136+
</div>
137+
138+
{/* Category */}
139+
<span
140+
className={`px-3 py-1 rounded-md text-sm font-medium ${defaultCategoryColors[category] || "bg-gray-100 text-gray-700"}`}
141+
>
142+
{category}
143+
</span>
180144
</div>
181-
)}
145+
))}
182146
</div>
183147
</div>,
184148
portalRoot

0 commit comments

Comments
 (0)