feature fix branch
This commit is contained in:
parent
4279cd291d
commit
faf1207300
@ -36,7 +36,9 @@
|
|||||||
"Bash(xargs -r docker rm:*)",
|
"Bash(xargs -r docker rm:*)",
|
||||||
"Bash(npm run migration:run:*)",
|
"Bash(npm run migration:run:*)",
|
||||||
"Bash(npm run dev:*)",
|
"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": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@ -24,15 +24,46 @@ async function createTestBooking() {
|
|||||||
const userId = '8cf7d5b3-d94f-44aa-bb5a-080002919dd1'; // User demo@xpeditis.com
|
const userId = '8cf7d5b3-d94f-44aa-bb5a-080002919dd1'; // User demo@xpeditis.com
|
||||||
const organizationId = '199fafa9-d26f-4cf9-9206-73432baa8f63';
|
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 = `
|
const query = `
|
||||||
INSERT INTO csv_bookings (
|
INSERT INTO csv_bookings (
|
||||||
id, user_id, organization_id, carrier_name, carrier_email,
|
id, user_id, organization_id, carrier_name, carrier_email,
|
||||||
origin, destination, volume_cbm, weight_kg, pallet_count,
|
origin, destination, volume_cbm, weight_kg, pallet_count,
|
||||||
price_usd, price_eur, primary_currency, transit_days, container_type,
|
price_usd, price_eur, primary_currency, transit_days, container_type,
|
||||||
status, confirmation_token, requested_at, notes
|
status, confirmation_token, requested_at, notes, documents
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
$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;
|
) RETURNING id, confirmation_token;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -55,6 +86,7 @@ async function createTestBooking() {
|
|||||||
'PENDING', // status - IMPORTANT!
|
'PENDING', // status - IMPORTANT!
|
||||||
confirmationToken,
|
confirmationToken,
|
||||||
'Test booking created by script',
|
'Test booking created by script',
|
||||||
|
dummyDocuments, // documents JSONB
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = await client.query(query, values);
|
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 { Module, forwardRef } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { CsvBookingsController } from './controllers/csv-bookings.controller';
|
import { CsvBookingsController } from './controllers/csv-bookings.controller';
|
||||||
|
import { CsvBookingActionsController } from './controllers/csv-booking-actions.controller';
|
||||||
import { CsvBookingService } from './services/csv-booking.service';
|
import { CsvBookingService } from './services/csv-booking.service';
|
||||||
import { CsvBookingOrmEntity } from '../infrastructure/persistence/typeorm/entities/csv-booking.orm-entity';
|
import { CsvBookingOrmEntity } from '../infrastructure/persistence/typeorm/entities/csv-booking.orm-entity';
|
||||||
import { TypeOrmCsvBookingRepository } from '../infrastructure/persistence/typeorm/repositories/csv-booking.repository';
|
import { TypeOrmCsvBookingRepository } from '../infrastructure/persistence/typeorm/repositories/csv-booking.repository';
|
||||||
@ -22,7 +23,7 @@ import { CarrierPortalModule } from './modules/carrier-portal.module';
|
|||||||
StorageModule,
|
StorageModule,
|
||||||
forwardRef(() => CarrierPortalModule), // Import CarrierPortalModule to access CarrierAuthService
|
forwardRef(() => CarrierPortalModule), // Import CarrierPortalModule to access CarrierAuthService
|
||||||
],
|
],
|
||||||
controllers: [CsvBookingsController],
|
controllers: [CsvBookingsController, CsvBookingActionsController],
|
||||||
providers: [CsvBookingService, TypeOrmCsvBookingRepository],
|
providers: [CsvBookingService, TypeOrmCsvBookingRepository],
|
||||||
exports: [CsvBookingService],
|
exports: [CsvBookingService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -58,7 +58,9 @@ export class CarrierAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new organization for the carrier
|
// Create new organization for the carrier
|
||||||
|
const organizationId = uuidv4(); // Generate UUID for organization
|
||||||
const organization = this.organizationRepository.create({
|
const organization = this.organizationRepository.create({
|
||||||
|
id: organizationId, // Provide explicit ID since @PrimaryColumn requires it
|
||||||
name: carrierName,
|
name: carrierName,
|
||||||
type: 'CARRIER',
|
type: 'CARRIER',
|
||||||
isCarrier: true,
|
isCarrier: true,
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export class CsvBookingMapper {
|
|||||||
ormEntity.transitDays,
|
ormEntity.transitDays,
|
||||||
ormEntity.containerType,
|
ormEntity.containerType,
|
||||||
CsvBookingStatus[ormEntity.status] as CsvBookingStatus,
|
CsvBookingStatus[ormEntity.status] as CsvBookingStatus,
|
||||||
ormEntity.documents as CsvBookingDocument[],
|
(ormEntity.documents || []) as CsvBookingDocument[], // Ensure documents is always an array
|
||||||
ormEntity.confirmationToken,
|
ormEntity.confirmationToken,
|
||||||
ormEntity.requestedAt,
|
ormEntity.requestedAt,
|
||||||
ormEntity.respondedAt,
|
ormEntity.respondedAt,
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export default function CarrierAcceptPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Appeler l'API backend pour accepter le booking
|
// 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',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -40,17 +40,25 @@ export default function CarrierAcceptPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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
|
// Messages d'erreur personnalisés
|
||||||
let errorMessage = errorData.message || 'Erreur lors de l\'acceptation du booking';
|
let errorMessage = errorData.message || 'Erreur lors de l\'acceptation du booking';
|
||||||
|
|
||||||
if (errorMessage.includes('status ACCEPTED')) {
|
// Log pour debug
|
||||||
errorMessage = 'Ce booking a déjà été accepté. Vous ne pouvez pas l\'accepter à nouveau.';
|
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')) {
|
} else if (errorMessage.includes('status REJECTED')) {
|
||||||
errorMessage = 'Ce booking a déjà été refusé. Vous ne pouvez pas l\'accepter.';
|
errorMessage = 'Ce booking a déjà été refusé. Vous ne pouvez pas l\'accepter.';
|
||||||
} else if (errorMessage.includes('not found')) {
|
} else if (errorMessage.includes('not found') || errorMessage.includes('Booking not found')) {
|
||||||
errorMessage = 'Booking introuvable. Le lien peut avoir expiré.';
|
errorMessage = 'Booking introuvable. Le lien peut avoir expiré ou le token est invalide.';
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
@ -67,10 +75,7 @@ export default function CarrierAcceptPage() {
|
|||||||
setIsNewAccount(data.isNewAccount);
|
setIsNewAccount(data.isNewAccount);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
// Rediriger vers la page de détails après 2 secondes
|
// Redirection manuelle - plus de redirection automatique
|
||||||
setTimeout(() => {
|
|
||||||
router.push(`/carrier/dashboard/bookings/${data.bookingId}`);
|
|
||||||
}, 2000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error accepting booking:', err);
|
console.error('Error accepting booking:', err);
|
||||||
setError(err instanceof Error ? err.message : 'Erreur lors de l\'acceptation');
|
setError(err instanceof Error ? err.message : 'Erreur lors de l\'acceptation');
|
||||||
@ -140,14 +145,13 @@ export default function CarrierAcceptPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-center text-gray-600 mb-6">
|
<button
|
||||||
<Truck className="w-5 h-5 mr-2 animate-bounce" />
|
onClick={() => router.push(`/carrier/dashboard/bookings/${bookingId}`)}
|
||||||
<p>Redirection vers les détails de la réservation...</p>
|
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"
|
||||||
</div>
|
>
|
||||||
|
<Truck className="w-5 h-5 mr-2" />
|
||||||
<div className="text-sm text-gray-500">
|
Voir les détails de la réservation
|
||||||
Vous serez automatiquement redirigé dans 2 secondes
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export default function CarrierRejectPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Construire l'URL avec la raison en query param si fournie
|
// 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()) {
|
if (reason.trim()) {
|
||||||
url.searchParams.append('reason', reason.trim());
|
url.searchParams.append('reason', reason.trim());
|
||||||
}
|
}
|
||||||
@ -76,10 +76,7 @@ export default function CarrierRejectPage() {
|
|||||||
setShowSuccess(true);
|
setShowSuccess(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
// Rediriger vers la page de détails après 2 secondes
|
// Redirection manuelle - plus de redirection automatique
|
||||||
setTimeout(() => {
|
|
||||||
router.push(`/carrier/dashboard/bookings/${data.bookingId}`);
|
|
||||||
}, 2000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error rejecting booking:', err);
|
console.error('Error rejecting booking:', err);
|
||||||
setError(err instanceof Error ? err.message : 'Erreur lors du refus');
|
setError(err instanceof Error ? err.message : 'Erreur lors du refus');
|
||||||
@ -134,14 +131,13 @@ export default function CarrierRejectPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-center text-gray-600 mb-6">
|
<button
|
||||||
<Truck className="w-5 h-5 mr-2 animate-bounce" />
|
onClick={() => router.push(`/carrier/dashboard/bookings/${bookingId}`)}
|
||||||
<p>Redirection vers les détails de la réservation...</p>
|
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"
|
||||||
</div>
|
>
|
||||||
|
<Truck className="w-5 h-5 mr-2" />
|
||||||
<div className="text-sm text-gray-500">
|
Voir les détails de la réservation
|
||||||
Vous serez automatiquement redirigé dans 2 secondes
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user