feature fix branch
This commit is contained in:
parent
4279cd291d
commit
faf1207300
@ -36,7 +36,9 @@
|
||||
"Bash(xargs -r docker rm:*)",
|
||||
"Bash(npm run migration:run:*)",
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(npm run backend:dev:*)"
|
||||
"Bash(npm run backend:dev:*)",
|
||||
"Bash(env -i PATH=\"$PATH\" HOME=\"$HOME\" node:*)",
|
||||
"Bash(PGPASSWORD=xpeditis_dev_password psql -h localhost -U xpeditis -d xpeditis_dev -c:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@ -24,15 +24,46 @@ async function createTestBooking() {
|
||||
const userId = '8cf7d5b3-d94f-44aa-bb5a-080002919dd1'; // User demo@xpeditis.com
|
||||
const organizationId = '199fafa9-d26f-4cf9-9206-73432baa8f63';
|
||||
|
||||
// Create dummy documents in JSONB format
|
||||
const dummyDocuments = JSON.stringify([
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'BILL_OF_LADING',
|
||||
fileName: 'bill-of-lading.pdf',
|
||||
filePath: 'https://dummy-storage.com/documents/bill-of-lading.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 102400, // 100KB
|
||||
uploadedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'PACKING_LIST',
|
||||
fileName: 'packing-list.pdf',
|
||||
filePath: 'https://dummy-storage.com/documents/packing-list.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 51200, // 50KB
|
||||
uploadedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'COMMERCIAL_INVOICE',
|
||||
fileName: 'commercial-invoice.pdf',
|
||||
filePath: 'https://dummy-storage.com/documents/commercial-invoice.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 76800, // 75KB
|
||||
uploadedAt: new Date().toISOString(),
|
||||
},
|
||||
]);
|
||||
|
||||
const query = `
|
||||
INSERT INTO csv_bookings (
|
||||
id, user_id, organization_id, carrier_name, carrier_email,
|
||||
origin, destination, volume_cbm, weight_kg, pallet_count,
|
||||
price_usd, price_eur, primary_currency, transit_days, container_type,
|
||||
status, confirmation_token, requested_at, notes
|
||||
status, confirmation_token, requested_at, notes, documents
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||
$11, $12, $13, $14, $15, $16, $17, NOW(), $18
|
||||
$11, $12, $13, $14, $15, $16, $17, NOW(), $18, $19
|
||||
) RETURNING id, confirmation_token;
|
||||
`;
|
||||
|
||||
@ -55,6 +86,7 @@ async function createTestBooking() {
|
||||
'PENDING', // status - IMPORTANT!
|
||||
confirmationToken,
|
||||
'Test booking created by script',
|
||||
dummyDocuments, // documents JSONB
|
||||
];
|
||||
|
||||
const result = await client.query(query, values);
|
||||
|
||||
@ -0,0 +1,125 @@
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||
import { Public } from '../decorators/public.decorator';
|
||||
import { CsvBookingService } from '../services/csv-booking.service';
|
||||
import { CarrierAuthService } from '../services/carrier-auth.service';
|
||||
|
||||
/**
|
||||
* CSV Booking Actions Controller (Public Routes)
|
||||
*
|
||||
* Handles public accept/reject actions from carrier emails
|
||||
* Separated from main controller to avoid routing conflicts
|
||||
*/
|
||||
@ApiTags('CSV Booking Actions')
|
||||
@Controller('csv-booking-actions')
|
||||
export class CsvBookingActionsController {
|
||||
constructor(
|
||||
private readonly csvBookingService: CsvBookingService,
|
||||
private readonly carrierAuthService: CarrierAuthService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Accept a booking request (PUBLIC - token-based)
|
||||
*
|
||||
* GET /api/v1/csv-booking-actions/accept/:token
|
||||
*/
|
||||
@Public()
|
||||
@Get('accept/:token')
|
||||
@ApiOperation({
|
||||
summary: 'Accept booking request (public)',
|
||||
description:
|
||||
'Public endpoint for carriers to accept a booking via email link. Updates booking status and notifies the user.',
|
||||
})
|
||||
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Booking accepted successfully. Returns auto-login token and booking details.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Booking not found or invalid token' })
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'Booking cannot be accepted (invalid status or expired)',
|
||||
})
|
||||
async acceptBooking(@Param('token') token: string) {
|
||||
// 1. Accept the booking
|
||||
const booking = await this.csvBookingService.acceptBooking(token);
|
||||
|
||||
// 2. Create carrier account if it doesn't exist
|
||||
const { carrierId, userId, isNewAccount } =
|
||||
await this.carrierAuthService.createCarrierAccountIfNotExists(
|
||||
booking.carrierEmail,
|
||||
booking.carrierName
|
||||
);
|
||||
|
||||
// 3. Link the booking to the carrier
|
||||
await this.csvBookingService.linkBookingToCarrier(booking.id, carrierId);
|
||||
|
||||
// 4. Generate auto-login token
|
||||
const autoLoginToken = await this.carrierAuthService.generateAutoLoginToken(userId, carrierId);
|
||||
|
||||
// 5. Return JSON response for frontend to handle
|
||||
return {
|
||||
success: true,
|
||||
autoLoginToken,
|
||||
bookingId: booking.id,
|
||||
isNewAccount,
|
||||
action: 'accepted',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a booking request (PUBLIC - token-based)
|
||||
*
|
||||
* GET /api/v1/csv-booking-actions/reject/:token
|
||||
*/
|
||||
@Public()
|
||||
@Get('reject/:token')
|
||||
@ApiOperation({
|
||||
summary: 'Reject booking request (public)',
|
||||
description:
|
||||
'Public endpoint for carriers to reject a booking via email link. Updates booking status and notifies the user.',
|
||||
})
|
||||
@ApiParam({ name: 'token', description: 'Booking confirmation token (UUID)' })
|
||||
@ApiQuery({
|
||||
name: 'reason',
|
||||
required: false,
|
||||
description: 'Rejection reason',
|
||||
example: 'No capacity available',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Booking rejected successfully. Returns auto-login token and booking details.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Booking not found or invalid token' })
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'Booking cannot be rejected (invalid status or expired)',
|
||||
})
|
||||
async rejectBooking(@Param('token') token: string, @Query('reason') reason: string) {
|
||||
// 1. Reject the booking
|
||||
const booking = await this.csvBookingService.rejectBooking(token, reason);
|
||||
|
||||
// 2. Create carrier account if it doesn't exist
|
||||
const { carrierId, userId, isNewAccount } =
|
||||
await this.carrierAuthService.createCarrierAccountIfNotExists(
|
||||
booking.carrierEmail,
|
||||
booking.carrierName
|
||||
);
|
||||
|
||||
// 3. Link the booking to the carrier
|
||||
await this.csvBookingService.linkBookingToCarrier(booking.id, carrierId);
|
||||
|
||||
// 4. Generate auto-login token
|
||||
const autoLoginToken = await this.carrierAuthService.generateAutoLoginToken(userId, carrierId);
|
||||
|
||||
// 5. Return JSON response for frontend to handle
|
||||
return {
|
||||
success: true,
|
||||
autoLoginToken,
|
||||
bookingId: booking.id,
|
||||
isNewAccount,
|
||||
action: 'rejected',
|
||||
reason: reason || null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CsvBookingsController } from './controllers/csv-bookings.controller';
|
||||
import { CsvBookingActionsController } from './controllers/csv-booking-actions.controller';
|
||||
import { CsvBookingService } from './services/csv-booking.service';
|
||||
import { CsvBookingOrmEntity } from '../infrastructure/persistence/typeorm/entities/csv-booking.orm-entity';
|
||||
import { TypeOrmCsvBookingRepository } from '../infrastructure/persistence/typeorm/repositories/csv-booking.repository';
|
||||
@ -22,7 +23,7 @@ import { CarrierPortalModule } from './modules/carrier-portal.module';
|
||||
StorageModule,
|
||||
forwardRef(() => CarrierPortalModule), // Import CarrierPortalModule to access CarrierAuthService
|
||||
],
|
||||
controllers: [CsvBookingsController],
|
||||
controllers: [CsvBookingsController, CsvBookingActionsController],
|
||||
providers: [CsvBookingService, TypeOrmCsvBookingRepository],
|
||||
exports: [CsvBookingService],
|
||||
})
|
||||
|
||||
@ -58,7 +58,9 @@ export class CarrierAuthService {
|
||||
}
|
||||
|
||||
// Create new organization for the carrier
|
||||
const organizationId = uuidv4(); // Generate UUID for organization
|
||||
const organization = this.organizationRepository.create({
|
||||
id: organizationId, // Provide explicit ID since @PrimaryColumn requires it
|
||||
name: carrierName,
|
||||
type: 'CARRIER',
|
||||
isCarrier: true,
|
||||
|
||||
@ -33,7 +33,7 @@ export class CsvBookingMapper {
|
||||
ormEntity.transitDays,
|
||||
ormEntity.containerType,
|
||||
CsvBookingStatus[ormEntity.status] as CsvBookingStatus,
|
||||
ormEntity.documents as CsvBookingDocument[],
|
||||
(ormEntity.documents || []) as CsvBookingDocument[], // Ensure documents is always an array
|
||||
ormEntity.confirmationToken,
|
||||
ormEntity.requestedAt,
|
||||
ormEntity.respondedAt,
|
||||
|
||||
@ -32,7 +32,7 @@ export default function CarrierAcceptPage() {
|
||||
|
||||
try {
|
||||
// Appeler l'API backend pour accepter le booking
|
||||
const response = await fetch(`http://localhost:4000/api/v1/csv-bookings/accept/${token}`, {
|
||||
const response = await fetch(`http://localhost:4000/api/v1/csv-booking-actions/accept/${token}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -40,17 +40,25 @@ export default function CarrierAcceptPage() {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch (e) {
|
||||
errorData = { message: `Erreur HTTP ${response.status}` };
|
||||
}
|
||||
|
||||
// Messages d'erreur personnalisés
|
||||
let errorMessage = errorData.message || 'Erreur lors de l\'acceptation du booking';
|
||||
|
||||
if (errorMessage.includes('status ACCEPTED')) {
|
||||
errorMessage = 'Ce booking a déjà été accepté. Vous ne pouvez pas l\'accepter à nouveau.';
|
||||
// Log pour debug
|
||||
console.error('API Error:', errorMessage);
|
||||
|
||||
if (errorMessage.includes('status ACCEPTED') || errorMessage.includes('ACCEPTED')) {
|
||||
errorMessage = '⚠️ Ce booking a déjà été accepté.\n\nVous devez créer un NOUVEAU booking avec:\ncd apps/backend\nnode create-test-booking.js';
|
||||
} else if (errorMessage.includes('status REJECTED')) {
|
||||
errorMessage = 'Ce booking a déjà été refusé. Vous ne pouvez pas l\'accepter.';
|
||||
} else if (errorMessage.includes('not found')) {
|
||||
errorMessage = 'Booking introuvable. Le lien peut avoir expiré.';
|
||||
} else if (errorMessage.includes('not found') || errorMessage.includes('Booking not found')) {
|
||||
errorMessage = 'Booking introuvable. Le lien peut avoir expiré ou le token est invalide.';
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
@ -67,10 +75,7 @@ export default function CarrierAcceptPage() {
|
||||
setIsNewAccount(data.isNewAccount);
|
||||
setLoading(false);
|
||||
|
||||
// Rediriger vers la page de détails après 2 secondes
|
||||
setTimeout(() => {
|
||||
router.push(`/carrier/dashboard/bookings/${data.bookingId}`);
|
||||
}, 2000);
|
||||
// Redirection manuelle - plus de redirection automatique
|
||||
} catch (err) {
|
||||
console.error('Error accepting booking:', err);
|
||||
setError(err instanceof Error ? err.message : 'Erreur lors de l\'acceptation');
|
||||
@ -140,14 +145,13 @@ export default function CarrierAcceptPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-center text-gray-600 mb-6">
|
||||
<Truck className="w-5 h-5 mr-2 animate-bounce" />
|
||||
<p>Redirection vers les détails de la réservation...</p>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500">
|
||||
Vous serez automatiquement redirigé dans 2 secondes
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push(`/carrier/dashboard/bookings/${bookingId}`)}
|
||||
className="w-full px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold transition-colors flex items-center justify-center"
|
||||
>
|
||||
<Truck className="w-5 h-5 mr-2" />
|
||||
Voir les détails de la réservation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -35,7 +35,7 @@ export default function CarrierRejectPage() {
|
||||
|
||||
try {
|
||||
// Construire l'URL avec la raison en query param si fournie
|
||||
const url = new URL(`http://localhost:4000/api/v1/csv-bookings/reject/${token}`);
|
||||
const url = new URL(`http://localhost:4000/api/v1/csv-booking-actions/reject/${token}`);
|
||||
if (reason.trim()) {
|
||||
url.searchParams.append('reason', reason.trim());
|
||||
}
|
||||
@ -76,10 +76,7 @@ export default function CarrierRejectPage() {
|
||||
setShowSuccess(true);
|
||||
setLoading(false);
|
||||
|
||||
// Rediriger vers la page de détails après 2 secondes
|
||||
setTimeout(() => {
|
||||
router.push(`/carrier/dashboard/bookings/${data.bookingId}`);
|
||||
}, 2000);
|
||||
// Redirection manuelle - plus de redirection automatique
|
||||
} catch (err) {
|
||||
console.error('Error rejecting booking:', err);
|
||||
setError(err instanceof Error ? err.message : 'Erreur lors du refus');
|
||||
@ -134,14 +131,13 @@ export default function CarrierRejectPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-center text-gray-600 mb-6">
|
||||
<Truck className="w-5 h-5 mr-2 animate-bounce" />
|
||||
<p>Redirection vers les détails de la réservation...</p>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500">
|
||||
Vous serez automatiquement redirigé dans 2 secondes
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push(`/carrier/dashboard/bookings/${bookingId}`)}
|
||||
className="w-full px-6 py-3 bg-orange-600 text-white rounded-lg hover:bg-orange-700 font-semibold transition-colors flex items-center justify-center"
|
||||
>
|
||||
<Truck className="w-5 h-5 mr-2" />
|
||||
Voir les détails de la réservation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user