Skip to content

Commit e206222

Browse files
Banyel3claude
andcommitted
feat(api): reverse-geocode endpoint for the map picker
GET /geocode/reverse?lat&lng -> { label } via the TomTom provider's reverseGeocode (already existed, just needed a route). Any-authenticated, throttled 30/min (billed TomTom call), validates coords -> 400. api-client.reverseGeocode(). Backs the "drop a pin -> show its address" map picker. 26 geocode tests; curl-verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 43352e5 commit e206222

4 files changed

Lines changed: 36 additions & 1 deletion

File tree

apps/api/src/common/route-roles.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ describe('route role matrix', () => {
9393
it('geocode is any-authenticated (non-sensitive utility, no @Roles)', () => {
9494
expect(rolesOf(GeocodeController.prototype, 'geocode')).toBeUndefined();
9595
expect(rolesOf(GeocodeController.prototype, 'search')).toBeUndefined();
96+
expect(rolesOf(GeocodeController.prototype, 'reverse')).toBeUndefined();
9697
});
9798

9899
it('the shops catalog is any-authenticated (non-sensitive, no @Roles)', () => {

apps/api/src/maps/geocode.controller.spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('GeocodeController', () => {
1616
name: 'stub',
1717
geocode: jest.fn().mockResolvedValue(result),
1818
search: jest.fn().mockResolvedValue([result]),
19-
reverseGeocode: jest.fn(),
19+
reverseGeocode: jest.fn().mockResolvedValue('Tetuan, Zamboanga City'),
2020
route: jest.fn(),
2121
};
2222
ctrl = new GeocodeController(maps);
@@ -57,4 +57,16 @@ describe('GeocodeController', () => {
5757
expect(maps.search).toHaveBeenNthCalledWith(2, 'Tetuan', 5);
5858
});
5959
});
60+
61+
describe('reverse', () => {
62+
it('reverse-geocodes a valid point to a label', async () => {
63+
const out = await ctrl.reverse('6.92', '122.08');
64+
expect(maps.reverseGeocode).toHaveBeenCalledWith({ lat: 6.92, lng: 122.08 });
65+
expect(out).toEqual({ label: 'Tetuan, Zamboanga City' });
66+
});
67+
68+
it('rejects non-numeric coordinates', async () => {
69+
await expect(ctrl.reverse('x', '122')).rejects.toBeInstanceOf(BadRequestException);
70+
});
71+
});
6072
});

apps/api/src/maps/geocode.controller.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
1010
import { Throttle } from '@nestjs/throttler';
1111
import type { GeocodeResult, MapsProvider } from '@wash-and-go/maps';
1212
import { RolesGuard } from '../common/guards/roles.guard';
13+
import { parseFiniteNumber } from '../common/parse-num';
1314
import { MAPS_PROVIDER } from './maps.constants';
1415

1516
/*
@@ -53,4 +54,20 @@ export class GeocodeController {
5354
const n = Number(limit);
5455
return this.maps.search(query, Number.isFinite(n) && n > 0 ? n : 5);
5556
}
57+
58+
// Coordinates → a human-readable address for the map picker (label the pin the
59+
// user dropped). Billed TomTom call → throttled. { label: null } when unknown.
60+
@Throttle({ default: { limit: 30, ttl: 60_000 } })
61+
@Get('reverse')
62+
@ApiOperation({ summary: 'Reverse geocode a pinned point → address label' })
63+
async reverse(
64+
@Query('lat') lat: string,
65+
@Query('lng') lng: string,
66+
): Promise<{ label: string | null }> {
67+
const point = {
68+
lat: parseFiniteNumber(lat, 'lat'),
69+
lng: parseFiniteNumber(lng, 'lng'),
70+
};
71+
return { label: await this.maps.reverseGeocode(point) };
72+
}
5673
}

packages/api-client/src/client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,11 @@ export class ApiClient {
322322
);
323323
}
324324

325+
// Coordinates → address label (map picker: label the dropped pin).
326+
reverseGeocode(lat: number, lng: number): Promise<{ label: string | null }> {
327+
return this.request('GET', `/geocode/reverse?lat=${lat}&lng=${lng}`);
328+
}
329+
325330
// --- Admin: rider cash reconciliation ---
326331

327332
getRiderCashSummary(): Promise<RiderCashBalance[]> {

0 commit comments

Comments
 (0)