fix license2
This commit is contained in:
parent
1e544fffab
commit
2158031bbe
49
.claude/settings.local.json
Normal file
49
.claude/settings.local.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(../../mvnw clean compile)",
|
||||
"Bash(do sed -i '/^@Slf4j$/{ N; s/@Slf4j\\n@Slf4j/@Slf4j/; }' \"$file\")",
|
||||
"Bash(../../mvnw clean install -DskipTests)",
|
||||
"Bash(../mvnw clean compile)",
|
||||
"Bash(for file in InvoiceLineItemMapper.java InvoiceMapper.java SubscriptionMapper.java PaymentEventMapper.java PaymentMethodMapper.java)",
|
||||
"Bash(do if [ -f \"$file\" ])",
|
||||
"Bash(then mv \"$file\" \"$file.disabled\")",
|
||||
"Bash(fi)",
|
||||
"Bash(../mvnw clean install -DskipTests)",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(1)",
|
||||
"Bash(tee:*)",
|
||||
"Bash(../../../../../../mvnw clean install -DskipTests -pl domain/service)",
|
||||
"Bash(for service in PlanServiceImpl SubscriptionServiceImpl)",
|
||||
"Bash(do if [ -f \"$service.java\" ])",
|
||||
"Bash(then mv \"$service.java\" \"$service.java.disabled\")",
|
||||
"Bash(timeout:*)",
|
||||
"Bash(./mvnw spring-boot:run:*)",
|
||||
"Bash(mv:*)",
|
||||
"Bash(./mvnw:*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(netstat:*)",
|
||||
"Bash(findstr:*)",
|
||||
"Bash(for:*)",
|
||||
"Bash(then newname=\"$file%.disabled\")",
|
||||
"Bash(if [ ! -f \"$newname\" ])",
|
||||
"Bash(then mv \"$file\" \"$newname\")",
|
||||
"Bash(echo:*)",
|
||||
"Bash(done)",
|
||||
"Bash(__NEW_LINE__ echo \"2. Health endpoint:\")",
|
||||
"Bash(__NEW_LINE__ echo \"3. Users endpoint (protected):\")",
|
||||
"Bash(__NEW_LINE__ echo \"4. Companies endpoint (protected):\")",
|
||||
"Bash(__NEW_LINE__ echo \"5. Documents endpoint (protected):\")",
|
||||
"Bash(__NEW_LINE__ echo \"6. Export folders endpoint (protected):\")",
|
||||
"Bash(__NEW_LINE__ echo \"7. Quotes endpoint (protected):\")",
|
||||
"Bash(then newname=\"$file%.tmp\")",
|
||||
"Bash(TOKEN=\"eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTc1OTQxNjE5MywiZXhwIjoxNzU5NTAyNTkzfQ.sWjlJI2taGPmERcWNCao77i1H8JJRst7GovKKvrMSoh0qIVyX5QIHeG2iLxfPisy\")",
|
||||
"Bash(jq:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(bash:*)",
|
||||
"Bash(TOKEN=\"eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJ0ZXN0YWRtaW4iLCJpYXQiOjE3NTk0MTgxMzIsImV4cCI6MTc1OTUwNDUzMn0.0wKu5BTIEzPwDohKTfh7LgAJkujKynKU_176dADEzn0a_Ho81J_NjubD54P1lO_n\")"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@ -65,7 +65,7 @@ public class SubscriptionController {
|
||||
request.getCompanyId(),
|
||||
request.getPlanType(),
|
||||
request.getBillingCycle(),
|
||||
request.getPaymentMethodId(),
|
||||
request.getPaymentMethodId() != null ? request.getPaymentMethodId().toString() : null,
|
||||
request.getStartTrial(),
|
||||
request.getCustomTrialDays()
|
||||
);
|
||||
@ -99,14 +99,14 @@ public class SubscriptionController {
|
||||
log.debug("Récupération des abonnements - statut: {}, cycle: {}, client: {}",
|
||||
status, billingCycle, customerId);
|
||||
|
||||
Page<Subscription> subscriptions = subscriptionService.findSubscriptions(
|
||||
status, billingCycle, customerId, pageable);
|
||||
List<Subscription> subscriptions = subscriptionService.findSubscriptions(
|
||||
status, billingCycle, customerId, pageable.getPageNumber(), pageable.getPageSize());
|
||||
|
||||
List<SubscriptionDto.Summary> dtos = subscriptions.getContent().stream()
|
||||
List<SubscriptionDto.Summary> dtos = subscriptions.stream()
|
||||
.map(dtoMapper::toSummaryDto)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Page<SubscriptionDto.Summary> result = new PageImpl<>(dtos, pageable, subscriptions.getTotalElements());
|
||||
Page<SubscriptionDto.Summary> result = new PageImpl<>(dtos, pageable, dtos.size());
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
@ -167,7 +167,7 @@ public class SubscriptionController {
|
||||
}
|
||||
|
||||
if (request.getNewPaymentMethodId() != null) {
|
||||
subscriptionService.updatePaymentMethod(subscriptionId, request.getNewPaymentMethodId());
|
||||
subscriptionService.updatePaymentMethod(subscriptionId, request.getNewPaymentMethodId().toString());
|
||||
if (updated.isEmpty()) {
|
||||
updated = subscriptionService.findById(subscriptionId);
|
||||
}
|
||||
|
||||
@ -0,0 +1,120 @@
|
||||
package com.dh7789dev.xpeditis.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO pour les licences dans les réponses API
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class LicenseDto {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "La clé de licence est obligatoire")
|
||||
private String licenseKey;
|
||||
|
||||
@NotNull(message = "Le type de licence est obligatoire")
|
||||
private String type;
|
||||
|
||||
@NotNull(message = "Le statut de la licence est obligatoire")
|
||||
private String status;
|
||||
|
||||
@NotNull(message = "La date de début est obligatoire")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate startDate;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate expirationDate;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime issuedDate;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime expiryDate;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime gracePeriodEndDate;
|
||||
|
||||
private Integer maxUsers;
|
||||
private Set<String> featuresEnabled;
|
||||
private Boolean isActive;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime lastCheckedAt;
|
||||
|
||||
// ===== PROPRIÉTÉS CALCULÉES =====
|
||||
|
||||
/**
|
||||
* @return true si la licence est expirée
|
||||
*/
|
||||
public Boolean isExpired() {
|
||||
return expirationDate != null && expirationDate.isBefore(LocalDate.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la licence est active
|
||||
*/
|
||||
public Boolean isActive() {
|
||||
return "ACTIVE".equals(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la licence est valide
|
||||
*/
|
||||
public Boolean isValid() {
|
||||
return isActive() || isInGracePeriod();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la licence est en période de grâce
|
||||
*/
|
||||
public Boolean isInGracePeriod() {
|
||||
return "GRACE_PERIOD".equals(status)
|
||||
&& gracePeriodEndDate != null
|
||||
&& LocalDateTime.now().isBefore(gracePeriodEndDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours jusqu'à l'expiration
|
||||
*/
|
||||
public Long getDaysUntilExpiration() {
|
||||
return expirationDate != null ?
|
||||
java.time.temporal.ChronoUnit.DAYS.between(LocalDate.now(), expirationDate) :
|
||||
null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la licence nécessite une attention
|
||||
*/
|
||||
public Boolean requiresAttention() {
|
||||
return "SUSPENDED".equals(status)
|
||||
|| (isInGracePeriod() && getDaysRemainingInGracePeriod() <= 1)
|
||||
|| (getDaysUntilExpiration() != null && getDaysUntilExpiration() <= 7 && getDaysUntilExpiration() > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours restants en période de grâce
|
||||
*/
|
||||
public Long getDaysRemainingInGracePeriod() {
|
||||
if (gracePeriodEndDate == null || !isInGracePeriod()) {
|
||||
return 0L;
|
||||
}
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(LocalDateTime.now(), gracePeriodEndDate);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package com.dh7789dev.xpeditis.mapper;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.SubscriptionDto;
|
||||
import com.dh7789dev.xpeditis.dto.app.Subscription;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Mapper manuel pour convertir entre Subscription (domain) et SubscriptionDto (application)
|
||||
*/
|
||||
@Component
|
||||
public class SubscriptionDtoMapper {
|
||||
|
||||
/**
|
||||
* Convertit un domaine Subscription en DTO
|
||||
*/
|
||||
public SubscriptionDto toDto(Subscription subscription) {
|
||||
if (subscription == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SubscriptionDto dto = new SubscriptionDto();
|
||||
dto.setId(subscription.getId());
|
||||
dto.setStripeSubscriptionId(subscription.getStripeSubscriptionId());
|
||||
dto.setStripeCustomerId(subscription.getStripeCustomerId());
|
||||
dto.setStripePriceId(subscription.getStripePriceId());
|
||||
dto.setStatus(subscription.getStatus() != null ? subscription.getStatus().name() : null);
|
||||
dto.setCurrentPeriodStart(subscription.getCurrentPeriodStart());
|
||||
dto.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd());
|
||||
dto.setCancelAtPeriodEnd(subscription.isCancelAtPeriodEnd());
|
||||
dto.setBillingCycle(subscription.getBillingCycle() != null ? subscription.getBillingCycle().name() : null);
|
||||
dto.setNextBillingDate(subscription.getNextBillingDate());
|
||||
dto.setTrialEndDate(subscription.getTrialEndDate());
|
||||
dto.setCreatedAt(subscription.getCreatedAt());
|
||||
dto.setUpdatedAt(subscription.getUpdatedAt());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un domaine Subscription en DTO Summary
|
||||
*/
|
||||
public SubscriptionDto.Summary toSummaryDto(Subscription subscription) {
|
||||
if (subscription == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SubscriptionDto.Summary summary = new SubscriptionDto.Summary();
|
||||
summary.setId(subscription.getId());
|
||||
summary.setStripeSubscriptionId(subscription.getStripeSubscriptionId());
|
||||
summary.setStatus(subscription.getStatus() != null ? subscription.getStatus().name() : null);
|
||||
summary.setBillingCycle(subscription.getBillingCycle() != null ? subscription.getBillingCycle().name() : null);
|
||||
summary.setNextBillingDate(subscription.getNextBillingDate());
|
||||
summary.setCreatedAt(subscription.getCreatedAt());
|
||||
summary.setRequiresAttention(subscription.requiresAttention());
|
||||
summary.setDaysUntilNextBilling(subscription.getDaysUntilNextBilling());
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un domaine Subscription en DTO Detailed
|
||||
*/
|
||||
public SubscriptionDto.Detailed toDetailedDto(Subscription subscription) {
|
||||
if (subscription == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SubscriptionDto.Detailed detailed = new SubscriptionDto.Detailed();
|
||||
detailed.setId(subscription.getId());
|
||||
detailed.setStripeSubscriptionId(subscription.getStripeSubscriptionId());
|
||||
detailed.setStripeCustomerId(subscription.getStripeCustomerId());
|
||||
detailed.setStripePriceId(subscription.getStripePriceId());
|
||||
detailed.setStatus(subscription.getStatus() != null ? subscription.getStatus().name() : null);
|
||||
detailed.setCurrentPeriodStart(subscription.getCurrentPeriodStart());
|
||||
detailed.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd());
|
||||
detailed.setCancelAtPeriodEnd(subscription.isCancelAtPeriodEnd());
|
||||
detailed.setBillingCycle(subscription.getBillingCycle() != null ? subscription.getBillingCycle().name() : null);
|
||||
detailed.setNextBillingDate(subscription.getNextBillingDate());
|
||||
detailed.setTrialEndDate(subscription.getTrialEndDate());
|
||||
detailed.setCreatedAt(subscription.getCreatedAt());
|
||||
detailed.setUpdatedAt(subscription.getUpdatedAt());
|
||||
|
||||
return detailed;
|
||||
}
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.Subscription;
|
||||
import com.dh7789dev.xpeditis.dto.app.SubscriptionStatus;
|
||||
import com.dh7789dev.xpeditis.dto.app.Plan;
|
||||
import com.dh7789dev.xpeditis.dto.app.BillingCycle;
|
||||
import com.dh7789dev.xpeditis.port.in.SubscriptionManagementUseCase;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Service de gestion des abonnements avec intégration Stripe
|
||||
*/
|
||||
public interface SubscriptionService extends SubscriptionManagementUseCase {
|
||||
|
||||
/**
|
||||
* Crée un abonnement Stripe et la licence associée
|
||||
*/
|
||||
Subscription createSubscription(UUID companyId, Plan plan, BillingCycle billingCycle);
|
||||
|
||||
/**
|
||||
* Met à jour un abonnement depuis un webhook Stripe
|
||||
*/
|
||||
Subscription updateSubscriptionFromStripe(String stripeSubscriptionId, SubscriptionStatus newStatus);
|
||||
|
||||
/**
|
||||
* Change le plan d'un abonnement
|
||||
*/
|
||||
Subscription changePlan(UUID companyId, Plan newPlan, boolean immediate);
|
||||
|
||||
/**
|
||||
* Annule un abonnement
|
||||
*/
|
||||
Subscription cancelSubscription(UUID companyId, boolean immediate, String reason);
|
||||
|
||||
/**
|
||||
* Réactive un abonnement annulé
|
||||
*/
|
||||
Subscription reactivateSubscription(UUID companyId);
|
||||
|
||||
/**
|
||||
* Récupère l'abonnement actif d'une entreprise
|
||||
*/
|
||||
Subscription getActiveSubscription(UUID companyId);
|
||||
|
||||
/**
|
||||
* Traite un échec de paiement
|
||||
*/
|
||||
void handlePaymentFailure(String stripeSubscriptionId, String reason);
|
||||
|
||||
/**
|
||||
* Traite un paiement réussi
|
||||
*/
|
||||
void handlePaymentSuccess(String stripeSubscriptionId, String stripeInvoiceId);
|
||||
|
||||
/**
|
||||
* Récupère les abonnements nécessitant une attention
|
||||
*/
|
||||
List<Subscription> getSubscriptionsRequiringAttention();
|
||||
|
||||
/**
|
||||
* Démarre la période de grâce pour un abonnement
|
||||
*/
|
||||
void startGracePeriod(UUID subscriptionId);
|
||||
|
||||
/**
|
||||
* Suspend les abonnements impayés
|
||||
*/
|
||||
void suspendUnpaidSubscriptions();
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package com.dh7789dev.xpeditis.port.in;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.Subscription;
|
||||
import com.dh7789dev.xpeditis.dto.app.SubscriptionStatus;
|
||||
import com.dh7789dev.xpeditis.dto.app.Plan;
|
||||
import com.dh7789dev.xpeditis.dto.app.BillingCycle;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Service de gestion des abonnements avec intégration Stripe
|
||||
*/
|
||||
public interface SubscriptionService extends SubscriptionManagementUseCase {
|
||||
|
||||
/**
|
||||
* Crée un abonnement Stripe avec tous les paramètres
|
||||
*/
|
||||
Subscription createSubscription(UUID companyId, String planType, String billingCycle,
|
||||
String paymentMethodId, Boolean startTrial, Integer customTrialDays);
|
||||
|
||||
/**
|
||||
* Met à jour un abonnement depuis un webhook Stripe
|
||||
*/
|
||||
Subscription updateSubscriptionFromStripe(String stripeSubscriptionId, SubscriptionStatus newStatus);
|
||||
|
||||
/**
|
||||
* Recherche les abonnements avec filtres
|
||||
*/
|
||||
List<Subscription> findSubscriptions(String status, String billingCycle, String customerId, int page, int size);
|
||||
|
||||
/**
|
||||
* Trouve un abonnement par ID
|
||||
*/
|
||||
Optional<Subscription> findById(UUID subscriptionId);
|
||||
|
||||
/**
|
||||
* Change le plan d'un abonnement
|
||||
*/
|
||||
Subscription changeSubscriptionPlan(UUID subscriptionId, String newPlanType, String newBillingCycle,
|
||||
Boolean enableProration, Boolean immediateChange);
|
||||
|
||||
/**
|
||||
* Met à jour le moyen de paiement
|
||||
*/
|
||||
void updatePaymentMethod(UUID subscriptionId, String newPaymentMethodId);
|
||||
|
||||
/**
|
||||
* Planifie l'annulation à la fin de période
|
||||
*/
|
||||
Subscription scheduleForCancellation(UUID subscriptionId, String cancellationReason);
|
||||
|
||||
/**
|
||||
* Annule un abonnement immédiatement
|
||||
*/
|
||||
Subscription cancelSubscriptionImmediately(UUID subscriptionId, String reason);
|
||||
|
||||
/**
|
||||
* Réactive un abonnement annulé
|
||||
*/
|
||||
Subscription reactivateSubscription(UUID subscriptionId);
|
||||
|
||||
/**
|
||||
* Trouve les abonnements par entreprise
|
||||
*/
|
||||
List<Subscription> findByCompanyId(UUID companyId);
|
||||
|
||||
/**
|
||||
* Récupère les abonnements nécessitant une attention
|
||||
*/
|
||||
List<Subscription> findSubscriptionsRequiringAttention();
|
||||
|
||||
/**
|
||||
* Trouve les essais qui se terminent bientôt
|
||||
*/
|
||||
List<Subscription> findTrialsEndingSoon(int daysAhead);
|
||||
|
||||
/**
|
||||
* Traite un échec de paiement
|
||||
*/
|
||||
void handlePaymentFailure(String stripeSubscriptionId, String reason);
|
||||
|
||||
/**
|
||||
* Traite un paiement réussi
|
||||
*/
|
||||
void handlePaymentSuccess(String stripeSubscriptionId, String stripeInvoiceId);
|
||||
|
||||
/**
|
||||
* Démarre la période de grâce pour un abonnement
|
||||
*/
|
||||
void startGracePeriod(UUID subscriptionId);
|
||||
|
||||
/**
|
||||
* Suspend les abonnements impayés
|
||||
*/
|
||||
void suspendUnpaidSubscriptions();
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AddressServiceImpl implements AddressService{
|
||||
}
|
||||
|
||||
@ -1,244 +1,244 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.GoogleUserInfo;
|
||||
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
||||
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
||||
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||
import com.dh7789dev.xpeditis.dto.valueobject.Email;
|
||||
import com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber;
|
||||
import com.dh7789dev.xpeditis.dto.app.AuthProvider;
|
||||
import com.dh7789dev.xpeditis.dto.app.Role;
|
||||
import com.dh7789dev.xpeditis.exception.AuthenticationException;
|
||||
import com.dh7789dev.xpeditis.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Transactional
|
||||
public class AuthenticationServiceImpl implements AuthenticationService {
|
||||
|
||||
private final AuthenticationRepository authenticationRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final OAuth2Provider oAuth2Provider;
|
||||
private final CompanyService companyService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public AuthenticationResponse authenticate(AuthenticationRequest request) {
|
||||
log.info("Authenticating user with username/email: {}", request.getUsername());
|
||||
|
||||
// Validate input
|
||||
if (request.getUsername() == null || request.getUsername().trim().isEmpty()) {
|
||||
throw new AuthenticationException("Username or email is required");
|
||||
}
|
||||
if (request.getPassword() == null || request.getPassword().trim().isEmpty()) {
|
||||
throw new AuthenticationException("Password is required");
|
||||
}
|
||||
|
||||
return authenticationRepository.authenticate(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationResponse register(RegisterRequest request) {
|
||||
log.info("Registering new user with email: {}", request.getEmail());
|
||||
|
||||
// Validate business rules
|
||||
validateRegistrationRequest(request);
|
||||
|
||||
// Check if user already exists
|
||||
if (userRepository.existsByEmail(request.getEmail())) {
|
||||
throw new BusinessException("User with this email already exists");
|
||||
}
|
||||
|
||||
if (request.getUsername() != null && userRepository.existsByUsername(request.getUsername())) {
|
||||
throw new BusinessException("Username already taken");
|
||||
}
|
||||
|
||||
// Create and validate user account
|
||||
UserAccount userAccount = createUserAccountFromRequest(request);
|
||||
|
||||
return authenticationRepository.register(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationResponse authenticateWithGoogle(String googleToken) {
|
||||
log.info("Authenticating user with Google OAuth2");
|
||||
|
||||
if (googleToken == null || googleToken.trim().isEmpty()) {
|
||||
throw new AuthenticationException("Google token is required");
|
||||
}
|
||||
|
||||
// Validate Google token
|
||||
if (!oAuth2Provider.validateToken(googleToken)) {
|
||||
throw new AuthenticationException("Invalid Google token");
|
||||
}
|
||||
|
||||
// Get user info from Google
|
||||
Optional<GoogleUserInfo> googleUserInfo = oAuth2Provider.getUserInfo(googleToken);
|
||||
if (googleUserInfo.isEmpty()) {
|
||||
throw new AuthenticationException("Failed to retrieve user information from Google");
|
||||
}
|
||||
|
||||
GoogleUserInfo userInfo = googleUserInfo.get();
|
||||
|
||||
// Check if user exists by Google ID or email
|
||||
Optional<UserAccount> existingUser = userRepository.findByGoogleId(userInfo.getId());
|
||||
if (existingUser.isEmpty()) {
|
||||
existingUser = userRepository.findByEmail(userInfo.getEmail());
|
||||
}
|
||||
|
||||
UserAccount userAccount;
|
||||
if (existingUser.isPresent()) {
|
||||
// Update existing user
|
||||
userAccount = existingUser.get();
|
||||
updateUserFromGoogleInfo(userAccount, userInfo);
|
||||
} else {
|
||||
// Create new user from Google info
|
||||
userAccount = createUserAccountFromGoogleInfo(userInfo);
|
||||
}
|
||||
|
||||
// Save/update user
|
||||
userAccount = userRepository.save(userAccount);
|
||||
|
||||
return authenticationRepository.authenticateWithGoogle(userAccount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAccount getCurrentUser(String token) {
|
||||
if (token == null || token.trim().isEmpty()) {
|
||||
throw new AuthenticationException("Token is required");
|
||||
}
|
||||
|
||||
return authenticationRepository.getCurrentUser(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(String token) {
|
||||
if (token == null || token.trim().isEmpty()) {
|
||||
throw new AuthenticationException("Token is required");
|
||||
}
|
||||
|
||||
log.info("Logging out user with token");
|
||||
authenticationRepository.logout(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateToken(String token) {
|
||||
if (token == null || token.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return authenticationRepository.validateToken(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationResponse refreshToken(String refreshToken) {
|
||||
if (refreshToken == null || refreshToken.trim().isEmpty()) {
|
||||
throw new AuthenticationException("Refresh token is required");
|
||||
}
|
||||
|
||||
log.info("Refreshing token");
|
||||
return authenticationRepository.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
private void validateRegistrationRequest(RegisterRequest request) {
|
||||
if (request.getEmail() == null || request.getEmail().trim().isEmpty()) {
|
||||
throw new BusinessException("Email is required");
|
||||
}
|
||||
|
||||
if (request.getFirstName() == null || request.getFirstName().trim().isEmpty()) {
|
||||
throw new BusinessException("First name is required");
|
||||
}
|
||||
|
||||
if (request.getLastName() == null || request.getLastName().trim().isEmpty()) {
|
||||
throw new BusinessException("Last name is required");
|
||||
}
|
||||
|
||||
if (request.getPassword() == null || request.getPassword().length() < 8) {
|
||||
throw new BusinessException("Password must be at least 8 characters long");
|
||||
}
|
||||
|
||||
// Validate email format by creating Email value object
|
||||
try {
|
||||
new Email(request.getEmail());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BusinessException("Invalid email format");
|
||||
}
|
||||
|
||||
// Validate phone number if provided
|
||||
if (request.getPhoneNumber() != null && !request.getPhoneNumber().trim().isEmpty()) {
|
||||
try {
|
||||
new PhoneNumber(request.getPhoneNumber());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BusinessException("Invalid phone number format");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private UserAccount createUserAccountFromRequest(RegisterRequest request) {
|
||||
return UserAccount.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.firstName(request.getFirstName().trim())
|
||||
.lastName(request.getLastName().trim())
|
||||
.email(new Email(request.getEmail()))
|
||||
.phoneNumber(request.getPhoneNumber() != null ? new PhoneNumber(request.getPhoneNumber()) : null)
|
||||
.username(request.getUsername() != null ? request.getUsername().trim() : null)
|
||||
.password(passwordEncoder.encode(request.getPassword()))
|
||||
.authProvider(AuthProvider.LOCAL)
|
||||
.role(Role.USER)
|
||||
.isActive(true)
|
||||
.privacyPolicyAccepted(request.isPrivacyPolicyAccepted())
|
||||
.privacyPolicyAcceptedAt(request.isPrivacyPolicyAccepted() ? LocalDateTime.now() : null)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
private UserAccount createUserAccountFromGoogleInfo(GoogleUserInfo googleInfo) {
|
||||
return UserAccount.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.firstName(googleInfo.getFirstName())
|
||||
.lastName(googleInfo.getLastName())
|
||||
.email(new Email(googleInfo.getEmail()))
|
||||
.googleId(googleInfo.getId())
|
||||
.authProvider(AuthProvider.GOOGLE)
|
||||
.role(Role.USER)
|
||||
.isActive(true)
|
||||
.privacyPolicyAccepted(false) // User will need to accept on first login
|
||||
.createdAt(LocalDateTime.now())
|
||||
.lastLoginAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
private void updateUserFromGoogleInfo(UserAccount userAccount, GoogleUserInfo googleInfo) {
|
||||
// Update Google ID if not set
|
||||
if (userAccount.getGoogleId() == null) {
|
||||
userAccount.setGoogleId(googleInfo.getId());
|
||||
}
|
||||
|
||||
// Update auth provider if it was local
|
||||
if (userAccount.getAuthProvider() == AuthProvider.LOCAL) {
|
||||
userAccount.setAuthProvider(AuthProvider.GOOGLE);
|
||||
}
|
||||
|
||||
// Update last login time
|
||||
userAccount.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
// Optionally update profile information if changed
|
||||
if (!googleInfo.getFirstName().equals(userAccount.getFirstName())) {
|
||||
userAccount.setFirstName(googleInfo.getFirstName());
|
||||
}
|
||||
if (!googleInfo.getLastName().equals(userAccount.getLastName())) {
|
||||
userAccount.setLastName(googleInfo.getLastName());
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.GoogleUserInfo;
|
||||
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
||||
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
||||
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||
import com.dh7789dev.xpeditis.dto.valueobject.Email;
|
||||
import com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber;
|
||||
import com.dh7789dev.xpeditis.dto.app.AuthProvider;
|
||||
import com.dh7789dev.xpeditis.dto.app.Role;
|
||||
import com.dh7789dev.xpeditis.exception.AuthenticationException;
|
||||
import com.dh7789dev.xpeditis.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class AuthenticationServiceImpl implements AuthenticationService {
|
||||
|
||||
private final AuthenticationRepository authenticationRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final OAuth2Provider oAuth2Provider;
|
||||
private final CompanyService companyService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public AuthenticationResponse authenticate(AuthenticationRequest request) {
|
||||
log.info("Authenticating user with username/email: {}", request.getUsername());
|
||||
|
||||
// Validate input
|
||||
if (request.getUsername() == null || request.getUsername().trim().isEmpty()) {
|
||||
throw new AuthenticationException("Username or email is required");
|
||||
}
|
||||
if (request.getPassword() == null || request.getPassword().trim().isEmpty()) {
|
||||
throw new AuthenticationException("Password is required");
|
||||
}
|
||||
|
||||
return authenticationRepository.authenticate(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationResponse register(RegisterRequest request) {
|
||||
log.info("Registering new user with email: {}", request.getEmail());
|
||||
|
||||
// Validate business rules
|
||||
validateRegistrationRequest(request);
|
||||
|
||||
// Check if user already exists
|
||||
if (userRepository.existsByEmail(request.getEmail())) {
|
||||
throw new BusinessException("User with this email already exists");
|
||||
}
|
||||
|
||||
if (request.getUsername() != null && userRepository.existsByUsername(request.getUsername())) {
|
||||
throw new BusinessException("Username already taken");
|
||||
}
|
||||
|
||||
// Create and validate user account
|
||||
UserAccount userAccount = createUserAccountFromRequest(request);
|
||||
|
||||
return authenticationRepository.register(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationResponse authenticateWithGoogle(String googleToken) {
|
||||
log.info("Authenticating user with Google OAuth2");
|
||||
|
||||
if (googleToken == null || googleToken.trim().isEmpty()) {
|
||||
throw new AuthenticationException("Google token is required");
|
||||
}
|
||||
|
||||
// Validate Google token
|
||||
if (!oAuth2Provider.validateToken(googleToken)) {
|
||||
throw new AuthenticationException("Invalid Google token");
|
||||
}
|
||||
|
||||
// Get user info from Google
|
||||
Optional<GoogleUserInfo> googleUserInfo = oAuth2Provider.getUserInfo(googleToken);
|
||||
if (googleUserInfo.isEmpty()) {
|
||||
throw new AuthenticationException("Failed to retrieve user information from Google");
|
||||
}
|
||||
|
||||
GoogleUserInfo userInfo = googleUserInfo.get();
|
||||
|
||||
// Check if user exists by Google ID or email
|
||||
Optional<UserAccount> existingUser = userRepository.findByGoogleId(userInfo.getId());
|
||||
if (existingUser.isEmpty()) {
|
||||
existingUser = userRepository.findByEmail(userInfo.getEmail());
|
||||
}
|
||||
|
||||
UserAccount userAccount;
|
||||
if (existingUser.isPresent()) {
|
||||
// Update existing user
|
||||
userAccount = existingUser.get();
|
||||
updateUserFromGoogleInfo(userAccount, userInfo);
|
||||
} else {
|
||||
// Create new user from Google info
|
||||
userAccount = createUserAccountFromGoogleInfo(userInfo);
|
||||
}
|
||||
|
||||
// Save/update user
|
||||
userAccount = userRepository.save(userAccount);
|
||||
|
||||
return authenticationRepository.authenticateWithGoogle(userAccount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAccount getCurrentUser(String token) {
|
||||
if (token == null || token.trim().isEmpty()) {
|
||||
throw new AuthenticationException("Token is required");
|
||||
}
|
||||
|
||||
return authenticationRepository.getCurrentUser(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(String token) {
|
||||
if (token == null || token.trim().isEmpty()) {
|
||||
throw new AuthenticationException("Token is required");
|
||||
}
|
||||
|
||||
log.info("Logging out user with token");
|
||||
authenticationRepository.logout(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateToken(String token) {
|
||||
if (token == null || token.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return authenticationRepository.validateToken(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationResponse refreshToken(String refreshToken) {
|
||||
if (refreshToken == null || refreshToken.trim().isEmpty()) {
|
||||
throw new AuthenticationException("Refresh token is required");
|
||||
}
|
||||
|
||||
log.info("Refreshing token");
|
||||
return authenticationRepository.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
private void validateRegistrationRequest(RegisterRequest request) {
|
||||
if (request.getEmail() == null || request.getEmail().trim().isEmpty()) {
|
||||
throw new BusinessException("Email is required");
|
||||
}
|
||||
|
||||
if (request.getFirstName() == null || request.getFirstName().trim().isEmpty()) {
|
||||
throw new BusinessException("First name is required");
|
||||
}
|
||||
|
||||
if (request.getLastName() == null || request.getLastName().trim().isEmpty()) {
|
||||
throw new BusinessException("Last name is required");
|
||||
}
|
||||
|
||||
if (request.getPassword() == null || request.getPassword().length() < 8) {
|
||||
throw new BusinessException("Password must be at least 8 characters long");
|
||||
}
|
||||
|
||||
// Validate email format by creating Email value object
|
||||
try {
|
||||
new Email(request.getEmail());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BusinessException("Invalid email format");
|
||||
}
|
||||
|
||||
// Validate phone number if provided
|
||||
if (request.getPhoneNumber() != null && !request.getPhoneNumber().trim().isEmpty()) {
|
||||
try {
|
||||
new PhoneNumber(request.getPhoneNumber());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BusinessException("Invalid phone number format");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private UserAccount createUserAccountFromRequest(RegisterRequest request) {
|
||||
return UserAccount.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.firstName(request.getFirstName().trim())
|
||||
.lastName(request.getLastName().trim())
|
||||
.email(new Email(request.getEmail()))
|
||||
.phoneNumber(request.getPhoneNumber() != null ? new PhoneNumber(request.getPhoneNumber()) : null)
|
||||
.username(request.getUsername() != null ? request.getUsername().trim() : null)
|
||||
.password(passwordEncoder.encode(request.getPassword()))
|
||||
.authProvider(AuthProvider.LOCAL)
|
||||
.role(Role.USER)
|
||||
.isActive(true)
|
||||
.privacyPolicyAccepted(request.isPrivacyPolicyAccepted())
|
||||
.privacyPolicyAcceptedAt(request.isPrivacyPolicyAccepted() ? LocalDateTime.now() : null)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
private UserAccount createUserAccountFromGoogleInfo(GoogleUserInfo googleInfo) {
|
||||
return UserAccount.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.firstName(googleInfo.getFirstName())
|
||||
.lastName(googleInfo.getLastName())
|
||||
.email(new Email(googleInfo.getEmail()))
|
||||
.googleId(googleInfo.getId())
|
||||
.authProvider(AuthProvider.GOOGLE)
|
||||
.role(Role.USER)
|
||||
.isActive(true)
|
||||
.privacyPolicyAccepted(false) // User will need to accept on first login
|
||||
.createdAt(LocalDateTime.now())
|
||||
.lastLoginAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
private void updateUserFromGoogleInfo(UserAccount userAccount, GoogleUserInfo googleInfo) {
|
||||
// Update Google ID if not set
|
||||
if (userAccount.getGoogleId() == null) {
|
||||
userAccount.setGoogleId(googleInfo.getId());
|
||||
}
|
||||
|
||||
// Update auth provider if it was local
|
||||
if (userAccount.getAuthProvider() == AuthProvider.LOCAL) {
|
||||
userAccount.setAuthProvider(AuthProvider.GOOGLE);
|
||||
}
|
||||
|
||||
// Update last login time
|
||||
userAccount.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
// Optionally update profile information if changed
|
||||
if (!googleInfo.getFirstName().equals(userAccount.getFirstName())) {
|
||||
userAccount.setFirstName(googleInfo.getFirstName());
|
||||
}
|
||||
if (!googleInfo.getLastName().equals(userAccount.getLastName())) {
|
||||
userAccount.setLastName(googleInfo.getLastName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,9 +19,9 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Transactional
|
||||
public class CompanyServiceImpl implements CompanyService {
|
||||
|
||||
|
||||
@ -1,478 +1,478 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DevisCalculServiceImpl implements DevisCalculService {
|
||||
|
||||
private final GrilleTarifaireService grilleTarifaireService;
|
||||
|
||||
private static final BigDecimal COEFFICIENT_POIDS_VOLUMETRIQUE = new BigDecimal("250"); // 250kg/m³
|
||||
|
||||
@Override
|
||||
public ReponseDevis calculerTroisOffres(DemandeDevis demandeDevis) {
|
||||
log.info("Calcul des 3 offres pour demande devis client: {}", demandeDevis.getNomClient());
|
||||
|
||||
validerDemandeDevis(demandeDevis);
|
||||
|
||||
// Calculer le colisage résumé
|
||||
ReponseDevis.ColisageResume colisageResume = calculerColisageResume(demandeDevis);
|
||||
|
||||
// Trouver les grilles applicables
|
||||
List<GrilleTarifaire> grillesApplicables = grilleTarifaireService.trouverGrillesApplicables(demandeDevis);
|
||||
|
||||
if (grillesApplicables.isEmpty()) {
|
||||
throw new IllegalStateException("Aucune grille tarifaire applicable pour cette demande");
|
||||
}
|
||||
|
||||
// Générer les 3 offres
|
||||
List<OffreCalculee> offres = genererTroisOffres(demandeDevis, grillesApplicables, colisageResume);
|
||||
|
||||
// Déterminer la recommandation
|
||||
ReponseDevis.Recommandation recommandation = determinerRecommandation(offres);
|
||||
|
||||
return new ReponseDevis(
|
||||
generateDemandeId(),
|
||||
new ReponseDevis.ClientInfo(demandeDevis.getNomClient(), demandeDevis.getEmailClient()),
|
||||
mapperDetailsTransport(demandeDevis),
|
||||
colisageResume,
|
||||
offres,
|
||||
recommandation,
|
||||
getMentionsLegales()
|
||||
);
|
||||
}
|
||||
|
||||
private List<OffreCalculee> genererTroisOffres(
|
||||
DemandeDevis demande,
|
||||
List<GrilleTarifaire> grilles,
|
||||
ReponseDevis.ColisageResume colisage) {
|
||||
|
||||
List<OffreCalculee> offres = new ArrayList<>();
|
||||
|
||||
// Pour chaque type de service (Rapide, Standard, Économique)
|
||||
for (GrilleTarifaire.ServiceType serviceType : GrilleTarifaire.ServiceType.values()) {
|
||||
|
||||
// Filtrer les grilles par type de service
|
||||
List<GrilleTarifaire> grillesService = grilles.stream()
|
||||
.filter(g -> g.getServiceType() == serviceType)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Si pas de grilles spécifiques, utiliser les grilles standard
|
||||
if (grillesService.isEmpty()) {
|
||||
grillesService = grilles.stream()
|
||||
.filter(g -> g.getServiceType() == GrilleTarifaire.ServiceType.STANDARD)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Calculer la meilleure offre pour ce type de service
|
||||
OffreCalculee meilleureOffre = calculerMeilleureOffre(demande, grillesService, serviceType, colisage);
|
||||
|
||||
if (meilleureOffre != null) {
|
||||
offres.add(appliquerAjustementParType(meilleureOffre, serviceType));
|
||||
}
|
||||
}
|
||||
|
||||
return offres;
|
||||
}
|
||||
|
||||
private OffreCalculee calculerMeilleureOffre(
|
||||
DemandeDevis demande,
|
||||
List<GrilleTarifaire> grilles,
|
||||
GrilleTarifaire.ServiceType serviceType,
|
||||
ReponseDevis.ColisageResume colisage) {
|
||||
|
||||
OffreCalculee meilleureOffre = null;
|
||||
BigDecimal prixMinimal = BigDecimal.valueOf(Double.MAX_VALUE);
|
||||
|
||||
for (GrilleTarifaire grille : grilles) {
|
||||
try {
|
||||
OffreCalculee offre = calculerOffreGrille(demande, grille, serviceType, colisage);
|
||||
|
||||
if (offre != null && offre.getPrixTotal().compareTo(prixMinimal) < 0) {
|
||||
prixMinimal = offre.getPrixTotal();
|
||||
meilleureOffre = offre;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Erreur lors du calcul avec la grille {}: {}", grille.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return meilleureOffre;
|
||||
}
|
||||
|
||||
private OffreCalculee calculerOffreGrille(
|
||||
DemandeDevis demande,
|
||||
GrilleTarifaire grille,
|
||||
GrilleTarifaire.ServiceType serviceType,
|
||||
ReponseDevis.ColisageResume colisage) {
|
||||
|
||||
// 1. Calculer le fret de base
|
||||
BigDecimal fretBase = calculerFretBase(grille, colisage);
|
||||
|
||||
// 2. Calculer les frais fixes obligatoires
|
||||
Map<String, BigDecimal> fraisFixes = calculerFraisFixes(grille, demande, colisage);
|
||||
|
||||
// 3. Calculer les surcharges marchandises dangereuses
|
||||
BigDecimal surchargeDangereuse = calculerSurchargeDangereuse(grille, demande, colisage);
|
||||
|
||||
// 4. Calculer les services optionnels demandés
|
||||
Map<String, BigDecimal> servicesOptionnels = calculerServicesOptionnels(grille, demande, colisage);
|
||||
|
||||
// 5. Calculer le prix total
|
||||
BigDecimal prixTotal = fretBase
|
||||
.add(fraisFixes.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add))
|
||||
.add(surchargeDangereuse)
|
||||
.add(servicesOptionnels.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
|
||||
// Créer les détails du prix
|
||||
OffreCalculee.DetailPrix detailPrix = new OffreCalculee.DetailPrix();
|
||||
detailPrix.setFretBase(fretBase);
|
||||
detailPrix.setFraisFixes(fraisFixes);
|
||||
detailPrix.setServicesOptionnels(servicesOptionnels);
|
||||
detailPrix.setSurchargeDangereuse(surchargeDangereuse);
|
||||
detailPrix.setCoefficientService(BigDecimal.ONE);
|
||||
|
||||
// Créer l'offre
|
||||
OffreCalculee offre = new OffreCalculee();
|
||||
offre.setType(serviceType.name());
|
||||
offre.setPrixTotal(prixTotal);
|
||||
offre.setDevise(grille.getDevise());
|
||||
offre.setTransitTime(formatTransitTime(grille));
|
||||
offre.setTransporteur(grille.getTransporteur());
|
||||
offre.setModeTransport(grille.getModeTransport().name());
|
||||
offre.setServicesInclus(getServicesInclus(serviceType));
|
||||
offre.setDetailPrix(detailPrix);
|
||||
offre.setValidite(LocalDate.now().plusDays(30));
|
||||
offre.setConditions(getConditions(serviceType));
|
||||
|
||||
return offre;
|
||||
}
|
||||
|
||||
private BigDecimal calculerFretBase(GrilleTarifaire grille, ReponseDevis.ColisageResume colisage) {
|
||||
|
||||
// Trouver le tarif applicable selon le poids taxable
|
||||
TarifFret tarifApplicable = grille.getTarifsFret().stream()
|
||||
.filter(t -> {
|
||||
BigDecimal poidsTaxable = BigDecimal.valueOf(colisage.getPoidsTaxable());
|
||||
return (t.getPoidsMin() == null || t.getPoidsMin().compareTo(poidsTaxable) <= 0) &&
|
||||
(t.getPoidsMax() == null || t.getPoidsMax().compareTo(poidsTaxable) >= 0);
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (tarifApplicable == null) {
|
||||
throw new IllegalStateException("Aucun tarif applicable pour le poids taxable: " + colisage.getPoidsTaxable());
|
||||
}
|
||||
|
||||
// Calculer le coût selon l'unité de facturation
|
||||
BigDecimal cout = BigDecimal.ZERO;
|
||||
|
||||
switch (tarifApplicable.getUniteFacturation()) {
|
||||
case KG:
|
||||
cout = BigDecimal.valueOf(colisage.getPoidsTaxable()).multiply(tarifApplicable.getTauxUnitaire());
|
||||
break;
|
||||
case M3:
|
||||
cout = BigDecimal.valueOf(colisage.getVolumeTotal()).multiply(tarifApplicable.getTauxUnitaire());
|
||||
break;
|
||||
case COLIS:
|
||||
cout = BigDecimal.valueOf(colisage.getNombreColis()).multiply(tarifApplicable.getTauxUnitaire());
|
||||
break;
|
||||
case LS:
|
||||
cout = tarifApplicable.getTauxUnitaire();
|
||||
break;
|
||||
}
|
||||
|
||||
// Appliquer le minimum de facturation si défini
|
||||
if (tarifApplicable.getMinimumFacturation() != null &&
|
||||
cout.compareTo(tarifApplicable.getMinimumFacturation()) < 0) {
|
||||
cout = tarifApplicable.getMinimumFacturation();
|
||||
}
|
||||
|
||||
return cout.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal> calculerFraisFixes(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
|
||||
Map<String, BigDecimal> fraisFixes = new HashMap<>();
|
||||
|
||||
for (FraisAdditionnels frais : grille.getFraisAdditionnels()) {
|
||||
if (frais.getObligatoire()) {
|
||||
BigDecimal montant = calculerMontantFrais(frais, demande, colisage);
|
||||
fraisFixes.put(frais.getTypeFrais(), montant);
|
||||
}
|
||||
}
|
||||
|
||||
return fraisFixes;
|
||||
}
|
||||
|
||||
private BigDecimal calculerSurchargeDangereuse(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
|
||||
if (demande.getMarchandiseDangereuse() == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
SurchargeDangereuse surcharge = grille.getSurchargesDangereuses().stream()
|
||||
.filter(s -> s.getClasseAdr().equals(demande.getMarchandiseDangereuse().getClasse()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (surcharge == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
BigDecimal montant = BigDecimal.ZERO;
|
||||
|
||||
switch (surcharge.getUniteFacturation()) {
|
||||
case KG:
|
||||
montant = BigDecimal.valueOf(colisage.getPoidsTaxable()).multiply(surcharge.getSurcharge());
|
||||
break;
|
||||
case COLIS:
|
||||
montant = BigDecimal.valueOf(colisage.getNombreColis()).multiply(surcharge.getSurcharge());
|
||||
break;
|
||||
case LS:
|
||||
montant = surcharge.getSurcharge();
|
||||
break;
|
||||
}
|
||||
|
||||
if (surcharge.getMinimum() != null && montant.compareTo(surcharge.getMinimum()) < 0) {
|
||||
montant = surcharge.getMinimum();
|
||||
}
|
||||
|
||||
return montant.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal> calculerServicesOptionnels(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
|
||||
Map<String, BigDecimal> services = new HashMap<>();
|
||||
|
||||
// Assurance
|
||||
if (demande.getServicesAdditionnels() != null && demande.getServicesAdditionnels().getAssurance()) {
|
||||
FraisAdditionnels fraisAssurance = grille.getFraisAdditionnels().stream()
|
||||
.filter(f -> "ASSURANCE".equals(f.getTypeFrais()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (fraisAssurance != null) {
|
||||
BigDecimal montant = calculerMontantFrais(fraisAssurance, demande, colisage);
|
||||
services.put("assurance", montant);
|
||||
}
|
||||
}
|
||||
|
||||
// Hayon
|
||||
if (demande.getManutentionParticuliere() != null && demande.getManutentionParticuliere().getHayon()) {
|
||||
FraisAdditionnels fraisHayon = grille.getFraisAdditionnels().stream()
|
||||
.filter(f -> "HAYON".equals(f.getTypeFrais()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (fraisHayon != null) {
|
||||
BigDecimal montant = calculerMontantFrais(fraisHayon, demande, colisage);
|
||||
services.put("hayon", montant);
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private BigDecimal calculerMontantFrais(FraisAdditionnels frais, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
|
||||
BigDecimal montant = BigDecimal.ZERO;
|
||||
|
||||
switch (frais.getUniteFacturation()) {
|
||||
case LS:
|
||||
montant = frais.getMontant();
|
||||
break;
|
||||
case KG:
|
||||
montant = BigDecimal.valueOf(colisage.getPoidsTaxable()).multiply(frais.getMontant());
|
||||
break;
|
||||
case M3:
|
||||
montant = BigDecimal.valueOf(colisage.getVolumeTotal()).multiply(frais.getMontant());
|
||||
break;
|
||||
case POURCENTAGE:
|
||||
// Pourcentage du fret de base - à implémenter selon le contexte
|
||||
montant = frais.getMontant();
|
||||
break;
|
||||
}
|
||||
|
||||
if (frais.getMontantMinimum() != null && montant.compareTo(frais.getMontantMinimum()) < 0) {
|
||||
montant = frais.getMontantMinimum();
|
||||
}
|
||||
|
||||
return montant.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private OffreCalculee appliquerAjustementParType(OffreCalculee offre, GrilleTarifaire.ServiceType serviceType) {
|
||||
BigDecimal coefficient = getCoefficient(serviceType);
|
||||
Double reductionTransit = getReductionTransitTime(serviceType);
|
||||
|
||||
// Appliquer le coefficient au prix total
|
||||
BigDecimal prixAjuste = offre.getPrixTotal().multiply(coefficient).setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
// Ajuster le détail des prix
|
||||
offre.getDetailPrix().setCoefficientService(coefficient);
|
||||
|
||||
// Mettre à jour le prix total
|
||||
offre.setPrixTotal(prixAjuste);
|
||||
|
||||
// Ajuster le transit time si défini
|
||||
if (reductionTransit != null) {
|
||||
String transitTimeAjuste = ajusterTransitTime(offre.getTransitTime(), reductionTransit);
|
||||
offre.setTransitTime(transitTimeAjuste);
|
||||
}
|
||||
|
||||
return offre;
|
||||
}
|
||||
|
||||
private BigDecimal getCoefficient(GrilleTarifaire.ServiceType serviceType) {
|
||||
switch (serviceType) {
|
||||
case RAPIDE:
|
||||
return new BigDecimal("1.15"); // +15%
|
||||
case STANDARD:
|
||||
return BigDecimal.ONE;
|
||||
case ECONOMIQUE:
|
||||
return new BigDecimal("0.85"); // -15%
|
||||
default:
|
||||
return BigDecimal.ONE;
|
||||
}
|
||||
}
|
||||
|
||||
private Double getReductionTransitTime(GrilleTarifaire.ServiceType serviceType) {
|
||||
switch (serviceType) {
|
||||
case RAPIDE:
|
||||
return 0.7; // -30%
|
||||
case STANDARD:
|
||||
return null; // Pas de changement
|
||||
case ECONOMIQUE:
|
||||
return 1.3; // +30%
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ReponseDevis.ColisageResume calculerColisageResume(DemandeDevis demande) {
|
||||
double poidsTotal = demande.getColisages().stream()
|
||||
.mapToDouble(c -> c.getPoids() * c.getQuantite())
|
||||
.sum();
|
||||
|
||||
double volumeTotal = demande.getColisages().stream()
|
||||
.mapToDouble(c -> c.getVolume() * c.getQuantite())
|
||||
.sum();
|
||||
|
||||
int nombreColis = demande.getColisages().stream()
|
||||
.mapToInt(DemandeDevis.Colisage::getQuantite)
|
||||
.sum();
|
||||
|
||||
// Calculer le poids taxable (max entre poids réel et poids volumétrique)
|
||||
double poidsVolumetrique = volumeTotal * COEFFICIENT_POIDS_VOLUMETRIQUE.doubleValue();
|
||||
double poidsTaxable = Math.max(poidsTotal, poidsVolumetrique);
|
||||
|
||||
return new ReponseDevis.ColisageResume(nombreColis, poidsTotal, volumeTotal, poidsTaxable);
|
||||
}
|
||||
|
||||
private List<String> getServicesInclus(GrilleTarifaire.ServiceType serviceType) {
|
||||
switch (serviceType) {
|
||||
case RAPIDE:
|
||||
return Arrays.asList("Suivi en temps réel", "Assurance de base", "Service express");
|
||||
case STANDARD:
|
||||
return Arrays.asList("Suivi standard");
|
||||
case ECONOMIQUE:
|
||||
return Collections.emptyList();
|
||||
default:
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getConditions(GrilleTarifaire.ServiceType serviceType) {
|
||||
switch (serviceType) {
|
||||
case RAPIDE:
|
||||
return Arrays.asList("Prix valable sous réserve d'espace disponible", "Marchandise prête à l'enlèvement");
|
||||
case STANDARD:
|
||||
return Arrays.asList("Prix standard selon grille tarifaire", "Délais indicatifs");
|
||||
case ECONOMIQUE:
|
||||
return Arrays.asList("Tarif économique avec délais étendus", "Services minimaux inclus");
|
||||
default:
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private ReponseDevis.Recommandation determinerRecommandation(List<OffreCalculee> offres) {
|
||||
// Par défaut, recommander l'offre STANDARD
|
||||
return new ReponseDevis.Recommandation("STANDARD", "Meilleur rapport qualité/prix/délai");
|
||||
}
|
||||
|
||||
private ReponseDevis.DetailsTransport mapperDetailsTransport(DemandeDevis demande) {
|
||||
ReponseDevis.DetailsTransport.AdresseInfo depart = new ReponseDevis.DetailsTransport.AdresseInfo(
|
||||
demande.getDepart().getVille() + ", " + demande.getDepart().getCodePostal() + ", " + demande.getDepart().getPays(),
|
||||
demande.getDepart().getCoordonneesGps()
|
||||
);
|
||||
|
||||
ReponseDevis.DetailsTransport.AdresseInfo arrivee = new ReponseDevis.DetailsTransport.AdresseInfo(
|
||||
demande.getArrivee().getVille() + ", " + demande.getArrivee().getCodePostal() + ", " + demande.getArrivee().getPays(),
|
||||
demande.getArrivee().getCoordonneesGps()
|
||||
);
|
||||
|
||||
return new ReponseDevis.DetailsTransport(demande.getTypeService(), demande.getIncoterm(), depart, arrivee);
|
||||
}
|
||||
|
||||
private String formatTransitTime(GrilleTarifaire grille) {
|
||||
if (grille.getTransitTimeMin() != null && grille.getTransitTimeMax() != null) {
|
||||
return grille.getTransitTimeMin() + "-" + grille.getTransitTimeMax() + " jours";
|
||||
}
|
||||
return "À définir";
|
||||
}
|
||||
|
||||
private String ajusterTransitTime(String transitTime, double coefficient) {
|
||||
// Extraction des nombres du format "25-30 jours" et application du coefficient
|
||||
if (transitTime.contains("-")) {
|
||||
String[] parts = transitTime.split("-");
|
||||
try {
|
||||
int min = (int) (Integer.parseInt(parts[0]) * coefficient);
|
||||
int max = (int) (Integer.parseInt(parts[1].split(" ")[0]) * coefficient);
|
||||
return min + "-" + max + " jours";
|
||||
} catch (NumberFormatException e) {
|
||||
return transitTime;
|
||||
}
|
||||
}
|
||||
return transitTime;
|
||||
}
|
||||
|
||||
private String generateDemandeId() {
|
||||
return "DEV-" + LocalDate.now().getYear() + "-" + String.format("%06d", new Random().nextInt(999999));
|
||||
}
|
||||
|
||||
private List<String> getMentionsLegales() {
|
||||
return Arrays.asList(
|
||||
"Prix hors taxes applicables",
|
||||
"Conditions générales de vente applicables",
|
||||
"Devis valable 30 jours"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validerDemandeDevis(DemandeDevis demandeDevis) {
|
||||
if (demandeDevis == null) {
|
||||
throw new IllegalArgumentException("La demande de devis ne peut pas être nulle");
|
||||
}
|
||||
|
||||
if (demandeDevis.getDepart() == null || demandeDevis.getArrivee() == null) {
|
||||
throw new IllegalArgumentException("Les adresses de départ et d'arrivée sont obligatoires");
|
||||
}
|
||||
|
||||
if (demandeDevis.getColisages() == null || demandeDevis.getColisages().isEmpty()) {
|
||||
throw new IllegalArgumentException("Au moins un colisage doit être défini");
|
||||
}
|
||||
|
||||
for (DemandeDevis.Colisage colisage : demandeDevis.getColisages()) {
|
||||
if (colisage.getPoids() == null || colisage.getPoids() <= 0) {
|
||||
throw new IllegalArgumentException("Le poids de chaque colisage doit être supérieur à 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DevisCalculServiceImpl implements DevisCalculService {
|
||||
|
||||
private final GrilleTarifaireService grilleTarifaireService;
|
||||
|
||||
private static final BigDecimal COEFFICIENT_POIDS_VOLUMETRIQUE = new BigDecimal("250"); // 250kg/m³
|
||||
|
||||
@Override
|
||||
public ReponseDevis calculerTroisOffres(DemandeDevis demandeDevis) {
|
||||
log.info("Calcul des 3 offres pour demande devis client: {}", demandeDevis.getNomClient());
|
||||
|
||||
validerDemandeDevis(demandeDevis);
|
||||
|
||||
// Calculer le colisage résumé
|
||||
ReponseDevis.ColisageResume colisageResume = calculerColisageResume(demandeDevis);
|
||||
|
||||
// Trouver les grilles applicables
|
||||
List<GrilleTarifaire> grillesApplicables = grilleTarifaireService.trouverGrillesApplicables(demandeDevis);
|
||||
|
||||
if (grillesApplicables.isEmpty()) {
|
||||
throw new IllegalStateException("Aucune grille tarifaire applicable pour cette demande");
|
||||
}
|
||||
|
||||
// Générer les 3 offres
|
||||
List<OffreCalculee> offres = genererTroisOffres(demandeDevis, grillesApplicables, colisageResume);
|
||||
|
||||
// Déterminer la recommandation
|
||||
ReponseDevis.Recommandation recommandation = determinerRecommandation(offres);
|
||||
|
||||
return new ReponseDevis(
|
||||
generateDemandeId(),
|
||||
new ReponseDevis.ClientInfo(demandeDevis.getNomClient(), demandeDevis.getEmailClient()),
|
||||
mapperDetailsTransport(demandeDevis),
|
||||
colisageResume,
|
||||
offres,
|
||||
recommandation,
|
||||
getMentionsLegales()
|
||||
);
|
||||
}
|
||||
|
||||
private List<OffreCalculee> genererTroisOffres(
|
||||
DemandeDevis demande,
|
||||
List<GrilleTarifaire> grilles,
|
||||
ReponseDevis.ColisageResume colisage) {
|
||||
|
||||
List<OffreCalculee> offres = new ArrayList<>();
|
||||
|
||||
// Pour chaque type de service (Rapide, Standard, Économique)
|
||||
for (GrilleTarifaire.ServiceType serviceType : GrilleTarifaire.ServiceType.values()) {
|
||||
|
||||
// Filtrer les grilles par type de service
|
||||
List<GrilleTarifaire> grillesService = grilles.stream()
|
||||
.filter(g -> g.getServiceType() == serviceType)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Si pas de grilles spécifiques, utiliser les grilles standard
|
||||
if (grillesService.isEmpty()) {
|
||||
grillesService = grilles.stream()
|
||||
.filter(g -> g.getServiceType() == GrilleTarifaire.ServiceType.STANDARD)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Calculer la meilleure offre pour ce type de service
|
||||
OffreCalculee meilleureOffre = calculerMeilleureOffre(demande, grillesService, serviceType, colisage);
|
||||
|
||||
if (meilleureOffre != null) {
|
||||
offres.add(appliquerAjustementParType(meilleureOffre, serviceType));
|
||||
}
|
||||
}
|
||||
|
||||
return offres;
|
||||
}
|
||||
|
||||
private OffreCalculee calculerMeilleureOffre(
|
||||
DemandeDevis demande,
|
||||
List<GrilleTarifaire> grilles,
|
||||
GrilleTarifaire.ServiceType serviceType,
|
||||
ReponseDevis.ColisageResume colisage) {
|
||||
|
||||
OffreCalculee meilleureOffre = null;
|
||||
BigDecimal prixMinimal = BigDecimal.valueOf(Double.MAX_VALUE);
|
||||
|
||||
for (GrilleTarifaire grille : grilles) {
|
||||
try {
|
||||
OffreCalculee offre = calculerOffreGrille(demande, grille, serviceType, colisage);
|
||||
|
||||
if (offre != null && offre.getPrixTotal().compareTo(prixMinimal) < 0) {
|
||||
prixMinimal = offre.getPrixTotal();
|
||||
meilleureOffre = offre;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Erreur lors du calcul avec la grille {}: {}", grille.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return meilleureOffre;
|
||||
}
|
||||
|
||||
private OffreCalculee calculerOffreGrille(
|
||||
DemandeDevis demande,
|
||||
GrilleTarifaire grille,
|
||||
GrilleTarifaire.ServiceType serviceType,
|
||||
ReponseDevis.ColisageResume colisage) {
|
||||
|
||||
// 1. Calculer le fret de base
|
||||
BigDecimal fretBase = calculerFretBase(grille, colisage);
|
||||
|
||||
// 2. Calculer les frais fixes obligatoires
|
||||
Map<String, BigDecimal> fraisFixes = calculerFraisFixes(grille, demande, colisage);
|
||||
|
||||
// 3. Calculer les surcharges marchandises dangereuses
|
||||
BigDecimal surchargeDangereuse = calculerSurchargeDangereuse(grille, demande, colisage);
|
||||
|
||||
// 4. Calculer les services optionnels demandés
|
||||
Map<String, BigDecimal> servicesOptionnels = calculerServicesOptionnels(grille, demande, colisage);
|
||||
|
||||
// 5. Calculer le prix total
|
||||
BigDecimal prixTotal = fretBase
|
||||
.add(fraisFixes.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add))
|
||||
.add(surchargeDangereuse)
|
||||
.add(servicesOptionnels.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
|
||||
// Créer les détails du prix
|
||||
OffreCalculee.DetailPrix detailPrix = new OffreCalculee.DetailPrix();
|
||||
detailPrix.setFretBase(fretBase);
|
||||
detailPrix.setFraisFixes(fraisFixes);
|
||||
detailPrix.setServicesOptionnels(servicesOptionnels);
|
||||
detailPrix.setSurchargeDangereuse(surchargeDangereuse);
|
||||
detailPrix.setCoefficientService(BigDecimal.ONE);
|
||||
|
||||
// Créer l'offre
|
||||
OffreCalculee offre = new OffreCalculee();
|
||||
offre.setType(serviceType.name());
|
||||
offre.setPrixTotal(prixTotal);
|
||||
offre.setDevise(grille.getDevise());
|
||||
offre.setTransitTime(formatTransitTime(grille));
|
||||
offre.setTransporteur(grille.getTransporteur());
|
||||
offre.setModeTransport(grille.getModeTransport().name());
|
||||
offre.setServicesInclus(getServicesInclus(serviceType));
|
||||
offre.setDetailPrix(detailPrix);
|
||||
offre.setValidite(LocalDate.now().plusDays(30));
|
||||
offre.setConditions(getConditions(serviceType));
|
||||
|
||||
return offre;
|
||||
}
|
||||
|
||||
private BigDecimal calculerFretBase(GrilleTarifaire grille, ReponseDevis.ColisageResume colisage) {
|
||||
|
||||
// Trouver le tarif applicable selon le poids taxable
|
||||
TarifFret tarifApplicable = grille.getTarifsFret().stream()
|
||||
.filter(t -> {
|
||||
BigDecimal poidsTaxable = BigDecimal.valueOf(colisage.getPoidsTaxable());
|
||||
return (t.getPoidsMin() == null || t.getPoidsMin().compareTo(poidsTaxable) <= 0) &&
|
||||
(t.getPoidsMax() == null || t.getPoidsMax().compareTo(poidsTaxable) >= 0);
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (tarifApplicable == null) {
|
||||
throw new IllegalStateException("Aucun tarif applicable pour le poids taxable: " + colisage.getPoidsTaxable());
|
||||
}
|
||||
|
||||
// Calculer le coût selon l'unité de facturation
|
||||
BigDecimal cout = BigDecimal.ZERO;
|
||||
|
||||
switch (tarifApplicable.getUniteFacturation()) {
|
||||
case KG:
|
||||
cout = BigDecimal.valueOf(colisage.getPoidsTaxable()).multiply(tarifApplicable.getTauxUnitaire());
|
||||
break;
|
||||
case M3:
|
||||
cout = BigDecimal.valueOf(colisage.getVolumeTotal()).multiply(tarifApplicable.getTauxUnitaire());
|
||||
break;
|
||||
case COLIS:
|
||||
cout = BigDecimal.valueOf(colisage.getNombreColis()).multiply(tarifApplicable.getTauxUnitaire());
|
||||
break;
|
||||
case LS:
|
||||
cout = tarifApplicable.getTauxUnitaire();
|
||||
break;
|
||||
}
|
||||
|
||||
// Appliquer le minimum de facturation si défini
|
||||
if (tarifApplicable.getMinimumFacturation() != null &&
|
||||
cout.compareTo(tarifApplicable.getMinimumFacturation()) < 0) {
|
||||
cout = tarifApplicable.getMinimumFacturation();
|
||||
}
|
||||
|
||||
return cout.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal> calculerFraisFixes(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
|
||||
Map<String, BigDecimal> fraisFixes = new HashMap<>();
|
||||
|
||||
for (FraisAdditionnels frais : grille.getFraisAdditionnels()) {
|
||||
if (frais.getObligatoire()) {
|
||||
BigDecimal montant = calculerMontantFrais(frais, demande, colisage);
|
||||
fraisFixes.put(frais.getTypeFrais(), montant);
|
||||
}
|
||||
}
|
||||
|
||||
return fraisFixes;
|
||||
}
|
||||
|
||||
private BigDecimal calculerSurchargeDangereuse(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
|
||||
if (demande.getMarchandiseDangereuse() == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
SurchargeDangereuse surcharge = grille.getSurchargesDangereuses().stream()
|
||||
.filter(s -> s.getClasseAdr().equals(demande.getMarchandiseDangereuse().getClasse()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (surcharge == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
BigDecimal montant = BigDecimal.ZERO;
|
||||
|
||||
switch (surcharge.getUniteFacturation()) {
|
||||
case KG:
|
||||
montant = BigDecimal.valueOf(colisage.getPoidsTaxable()).multiply(surcharge.getSurcharge());
|
||||
break;
|
||||
case COLIS:
|
||||
montant = BigDecimal.valueOf(colisage.getNombreColis()).multiply(surcharge.getSurcharge());
|
||||
break;
|
||||
case LS:
|
||||
montant = surcharge.getSurcharge();
|
||||
break;
|
||||
}
|
||||
|
||||
if (surcharge.getMinimum() != null && montant.compareTo(surcharge.getMinimum()) < 0) {
|
||||
montant = surcharge.getMinimum();
|
||||
}
|
||||
|
||||
return montant.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal> calculerServicesOptionnels(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
|
||||
Map<String, BigDecimal> services = new HashMap<>();
|
||||
|
||||
// Assurance
|
||||
if (demande.getServicesAdditionnels() != null && demande.getServicesAdditionnels().getAssurance()) {
|
||||
FraisAdditionnels fraisAssurance = grille.getFraisAdditionnels().stream()
|
||||
.filter(f -> "ASSURANCE".equals(f.getTypeFrais()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (fraisAssurance != null) {
|
||||
BigDecimal montant = calculerMontantFrais(fraisAssurance, demande, colisage);
|
||||
services.put("assurance", montant);
|
||||
}
|
||||
}
|
||||
|
||||
// Hayon
|
||||
if (demande.getManutentionParticuliere() != null && demande.getManutentionParticuliere().getHayon()) {
|
||||
FraisAdditionnels fraisHayon = grille.getFraisAdditionnels().stream()
|
||||
.filter(f -> "HAYON".equals(f.getTypeFrais()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (fraisHayon != null) {
|
||||
BigDecimal montant = calculerMontantFrais(fraisHayon, demande, colisage);
|
||||
services.put("hayon", montant);
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private BigDecimal calculerMontantFrais(FraisAdditionnels frais, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
|
||||
BigDecimal montant = BigDecimal.ZERO;
|
||||
|
||||
switch (frais.getUniteFacturation()) {
|
||||
case LS:
|
||||
montant = frais.getMontant();
|
||||
break;
|
||||
case KG:
|
||||
montant = BigDecimal.valueOf(colisage.getPoidsTaxable()).multiply(frais.getMontant());
|
||||
break;
|
||||
case M3:
|
||||
montant = BigDecimal.valueOf(colisage.getVolumeTotal()).multiply(frais.getMontant());
|
||||
break;
|
||||
case POURCENTAGE:
|
||||
// Pourcentage du fret de base - à implémenter selon le contexte
|
||||
montant = frais.getMontant();
|
||||
break;
|
||||
}
|
||||
|
||||
if (frais.getMontantMinimum() != null && montant.compareTo(frais.getMontantMinimum()) < 0) {
|
||||
montant = frais.getMontantMinimum();
|
||||
}
|
||||
|
||||
return montant.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private OffreCalculee appliquerAjustementParType(OffreCalculee offre, GrilleTarifaire.ServiceType serviceType) {
|
||||
BigDecimal coefficient = getCoefficient(serviceType);
|
||||
Double reductionTransit = getReductionTransitTime(serviceType);
|
||||
|
||||
// Appliquer le coefficient au prix total
|
||||
BigDecimal prixAjuste = offre.getPrixTotal().multiply(coefficient).setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
// Ajuster le détail des prix
|
||||
offre.getDetailPrix().setCoefficientService(coefficient);
|
||||
|
||||
// Mettre à jour le prix total
|
||||
offre.setPrixTotal(prixAjuste);
|
||||
|
||||
// Ajuster le transit time si défini
|
||||
if (reductionTransit != null) {
|
||||
String transitTimeAjuste = ajusterTransitTime(offre.getTransitTime(), reductionTransit);
|
||||
offre.setTransitTime(transitTimeAjuste);
|
||||
}
|
||||
|
||||
return offre;
|
||||
}
|
||||
|
||||
private BigDecimal getCoefficient(GrilleTarifaire.ServiceType serviceType) {
|
||||
switch (serviceType) {
|
||||
case RAPIDE:
|
||||
return new BigDecimal("1.15"); // +15%
|
||||
case STANDARD:
|
||||
return BigDecimal.ONE;
|
||||
case ECONOMIQUE:
|
||||
return new BigDecimal("0.85"); // -15%
|
||||
default:
|
||||
return BigDecimal.ONE;
|
||||
}
|
||||
}
|
||||
|
||||
private Double getReductionTransitTime(GrilleTarifaire.ServiceType serviceType) {
|
||||
switch (serviceType) {
|
||||
case RAPIDE:
|
||||
return 0.7; // -30%
|
||||
case STANDARD:
|
||||
return null; // Pas de changement
|
||||
case ECONOMIQUE:
|
||||
return 1.3; // +30%
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ReponseDevis.ColisageResume calculerColisageResume(DemandeDevis demande) {
|
||||
double poidsTotal = demande.getColisages().stream()
|
||||
.mapToDouble(c -> c.getPoids() * c.getQuantite())
|
||||
.sum();
|
||||
|
||||
double volumeTotal = demande.getColisages().stream()
|
||||
.mapToDouble(c -> c.getVolume() * c.getQuantite())
|
||||
.sum();
|
||||
|
||||
int nombreColis = demande.getColisages().stream()
|
||||
.mapToInt(DemandeDevis.Colisage::getQuantite)
|
||||
.sum();
|
||||
|
||||
// Calculer le poids taxable (max entre poids réel et poids volumétrique)
|
||||
double poidsVolumetrique = volumeTotal * COEFFICIENT_POIDS_VOLUMETRIQUE.doubleValue();
|
||||
double poidsTaxable = Math.max(poidsTotal, poidsVolumetrique);
|
||||
|
||||
return new ReponseDevis.ColisageResume(nombreColis, poidsTotal, volumeTotal, poidsTaxable);
|
||||
}
|
||||
|
||||
private List<String> getServicesInclus(GrilleTarifaire.ServiceType serviceType) {
|
||||
switch (serviceType) {
|
||||
case RAPIDE:
|
||||
return Arrays.asList("Suivi en temps réel", "Assurance de base", "Service express");
|
||||
case STANDARD:
|
||||
return Arrays.asList("Suivi standard");
|
||||
case ECONOMIQUE:
|
||||
return Collections.emptyList();
|
||||
default:
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getConditions(GrilleTarifaire.ServiceType serviceType) {
|
||||
switch (serviceType) {
|
||||
case RAPIDE:
|
||||
return Arrays.asList("Prix valable sous réserve d'espace disponible", "Marchandise prête à l'enlèvement");
|
||||
case STANDARD:
|
||||
return Arrays.asList("Prix standard selon grille tarifaire", "Délais indicatifs");
|
||||
case ECONOMIQUE:
|
||||
return Arrays.asList("Tarif économique avec délais étendus", "Services minimaux inclus");
|
||||
default:
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private ReponseDevis.Recommandation determinerRecommandation(List<OffreCalculee> offres) {
|
||||
// Par défaut, recommander l'offre STANDARD
|
||||
return new ReponseDevis.Recommandation("STANDARD", "Meilleur rapport qualité/prix/délai");
|
||||
}
|
||||
|
||||
private ReponseDevis.DetailsTransport mapperDetailsTransport(DemandeDevis demande) {
|
||||
ReponseDevis.DetailsTransport.AdresseInfo depart = new ReponseDevis.DetailsTransport.AdresseInfo(
|
||||
demande.getDepart().getVille() + ", " + demande.getDepart().getCodePostal() + ", " + demande.getDepart().getPays(),
|
||||
demande.getDepart().getCoordonneesGps()
|
||||
);
|
||||
|
||||
ReponseDevis.DetailsTransport.AdresseInfo arrivee = new ReponseDevis.DetailsTransport.AdresseInfo(
|
||||
demande.getArrivee().getVille() + ", " + demande.getArrivee().getCodePostal() + ", " + demande.getArrivee().getPays(),
|
||||
demande.getArrivee().getCoordonneesGps()
|
||||
);
|
||||
|
||||
return new ReponseDevis.DetailsTransport(demande.getTypeService(), demande.getIncoterm(), depart, arrivee);
|
||||
}
|
||||
|
||||
private String formatTransitTime(GrilleTarifaire grille) {
|
||||
if (grille.getTransitTimeMin() != null && grille.getTransitTimeMax() != null) {
|
||||
return grille.getTransitTimeMin() + "-" + grille.getTransitTimeMax() + " jours";
|
||||
}
|
||||
return "À définir";
|
||||
}
|
||||
|
||||
private String ajusterTransitTime(String transitTime, double coefficient) {
|
||||
// Extraction des nombres du format "25-30 jours" et application du coefficient
|
||||
if (transitTime.contains("-")) {
|
||||
String[] parts = transitTime.split("-");
|
||||
try {
|
||||
int min = (int) (Integer.parseInt(parts[0]) * coefficient);
|
||||
int max = (int) (Integer.parseInt(parts[1].split(" ")[0]) * coefficient);
|
||||
return min + "-" + max + " jours";
|
||||
} catch (NumberFormatException e) {
|
||||
return transitTime;
|
||||
}
|
||||
}
|
||||
return transitTime;
|
||||
}
|
||||
|
||||
private String generateDemandeId() {
|
||||
return "DEV-" + LocalDate.now().getYear() + "-" + String.format("%06d", new Random().nextInt(999999));
|
||||
}
|
||||
|
||||
private List<String> getMentionsLegales() {
|
||||
return Arrays.asList(
|
||||
"Prix hors taxes applicables",
|
||||
"Conditions générales de vente applicables",
|
||||
"Devis valable 30 jours"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validerDemandeDevis(DemandeDevis demandeDevis) {
|
||||
if (demandeDevis == null) {
|
||||
throw new IllegalArgumentException("La demande de devis ne peut pas être nulle");
|
||||
}
|
||||
|
||||
if (demandeDevis.getDepart() == null || demandeDevis.getArrivee() == null) {
|
||||
throw new IllegalArgumentException("Les adresses de départ et d'arrivée sont obligatoires");
|
||||
}
|
||||
|
||||
if (demandeDevis.getColisages() == null || demandeDevis.getColisages().isEmpty()) {
|
||||
throw new IllegalArgumentException("Au moins un colisage doit être défini");
|
||||
}
|
||||
|
||||
for (DemandeDevis.Colisage colisage : demandeDevis.getColisages()) {
|
||||
if (colisage.getPoids() == null || colisage.getPoids() <= 0) {
|
||||
throw new IllegalArgumentException("Le poids de chaque colisage doit être supérieur à 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DimensionServiceImpl implements DimensionService {
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DocumentServiceImpl implements DocumentService {
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ExportFolderServiceImpl implements ExportFolderService {
|
||||
}
|
||||
|
||||
@ -1,426 +1,426 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.DemandeDevis;
|
||||
import com.dh7789dev.xpeditis.dto.app.GrilleTarifaire;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class GrilleTarifaireServiceImpl implements GrilleTarifaireService {
|
||||
|
||||
private final GrilleTarifaireRepository grilleTarifaireRepository;
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> trouverGrillesApplicables(DemandeDevis demandeDevis) {
|
||||
log.info("Recherche des grilles tarifaires applicables pour {} -> {}",
|
||||
demandeDevis.getDepart().getPays(),
|
||||
demandeDevis.getArrivee().getPays());
|
||||
|
||||
LocalDate dateValidite = demandeDevis.getDateEnlevement() != null
|
||||
? demandeDevis.getDateEnlevement()
|
||||
: LocalDate.now();
|
||||
|
||||
List<GrilleTarifaire> grilles = grilleTarifaireRepository.findGrillesApplicables(
|
||||
demandeDevis.getTypeService(),
|
||||
demandeDevis.getDepart().getPays(),
|
||||
demandeDevis.getArrivee().getPays(),
|
||||
dateValidite
|
||||
);
|
||||
|
||||
// Filtrer par ville si spécifiée
|
||||
if (demandeDevis.getDepart().getVille() != null || demandeDevis.getArrivee().getVille() != null) {
|
||||
grilles = grilles.stream()
|
||||
.filter(g -> isVilleCompatible(g, demandeDevis))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Filtrer par incoterm si spécifié
|
||||
if (demandeDevis.getIncoterm() != null) {
|
||||
grilles = grilles.stream()
|
||||
.filter(g -> g.getIncoterm() == null || g.getIncoterm().equals(demandeDevis.getIncoterm()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
log.info("Trouvé {} grille(s) applicables", grilles.size());
|
||||
return grilles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrilleTarifaire sauvegarderGrille(GrilleTarifaire grilleTarifaire) {
|
||||
log.info("Sauvegarde de la grille tarifaire: {}", grilleTarifaire.getNomGrille());
|
||||
|
||||
validerGrilleTarifaire(grilleTarifaire);
|
||||
|
||||
return grilleTarifaireRepository.save(grilleTarifaire);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrilleTarifaire trouverParId(Long id) {
|
||||
log.debug("Recherche de la grille tarifaire avec l'ID: {}", id);
|
||||
|
||||
return grilleTarifaireRepository.findById(id)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void supprimerGrille(Long id) {
|
||||
log.info("Suppression de la grille tarifaire avec l'ID: {}", id);
|
||||
|
||||
if (!grilleTarifaireRepository.existsById(id)) {
|
||||
throw new IllegalArgumentException("Aucune grille tarifaire trouvée avec l'ID: " + id);
|
||||
}
|
||||
|
||||
grilleTarifaireRepository.deleteById(id);
|
||||
}
|
||||
|
||||
private boolean isVilleCompatible(GrilleTarifaire grille, DemandeDevis demande) {
|
||||
// Si la grille n'a pas de ville spécifiée, elle est compatible avec toutes les villes
|
||||
boolean origineCompatible = grille.getOrigineVille() == null ||
|
||||
grille.getOrigineVille().equalsIgnoreCase(demande.getDepart().getVille());
|
||||
|
||||
boolean destinationCompatible = grille.getDestinationVille() == null ||
|
||||
grille.getDestinationVille().equalsIgnoreCase(demande.getArrivee().getVille());
|
||||
|
||||
return origineCompatible && destinationCompatible;
|
||||
}
|
||||
|
||||
private void validerGrilleTarifaire(GrilleTarifaire grille) {
|
||||
if (grille == null) {
|
||||
throw new IllegalArgumentException("La grille tarifaire ne peut pas être nulle");
|
||||
}
|
||||
|
||||
if (grille.getNomGrille() == null || grille.getNomGrille().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le nom de la grille est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getTransporteur() == null || grille.getTransporteur().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le transporteur est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getTypeService() == null) {
|
||||
throw new IllegalArgumentException("Le type de service est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getOriginePays() == null || grille.getOriginePays().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le pays d'origine est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getDestinationPays() == null || grille.getDestinationPays().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le pays de destination est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getValiditeDebut() == null) {
|
||||
throw new IllegalArgumentException("La date de début de validité est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getValiditeFin() == null) {
|
||||
throw new IllegalArgumentException("La date de fin de validité est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getValiditeDebut().isAfter(grille.getValiditeFin())) {
|
||||
throw new IllegalArgumentException("La date de début doit être antérieure à la date de fin");
|
||||
}
|
||||
|
||||
// Validation des tarifs de fret
|
||||
if (grille.getTarifsFret() == null || grille.getTarifsFret().isEmpty()) {
|
||||
throw new IllegalArgumentException("Au moins un tarif de fret doit être défini");
|
||||
}
|
||||
|
||||
// Validation des codes pays (format ISO 3166-1 alpha-3)
|
||||
if (grille.getOriginePays().length() != 3) {
|
||||
throw new IllegalArgumentException("Le code pays d'origine doit être au format ISO 3166-1 alpha-3 (3 caractères)");
|
||||
}
|
||||
|
||||
if (grille.getDestinationPays().length() != 3) {
|
||||
throw new IllegalArgumentException("Le code pays de destination doit être au format ISO 3166-1 alpha-3 (3 caractères)");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> importerDepuisCsv(MultipartFile file, String mode) throws IOException {
|
||||
log.info("Import CSV - Fichier: {}, Mode: {}", file.getOriginalFilename(), mode);
|
||||
|
||||
if ("REPLACE".equalsIgnoreCase(mode)) {
|
||||
log.info("Mode REPLACE - Suppression de toutes les grilles existantes");
|
||||
grilleTarifaireRepository.deleteAll();
|
||||
}
|
||||
|
||||
List<GrilleTarifaire> grillesImportees = new ArrayList<>();
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), "UTF-8"))) {
|
||||
String headerLine = reader.readLine();
|
||||
if (headerLine == null) {
|
||||
throw new IllegalArgumentException("Le fichier CSV est vide");
|
||||
}
|
||||
|
||||
String[] headers = headerLine.split(",");
|
||||
log.debug("En-têtes CSV: {}", Arrays.toString(headers));
|
||||
|
||||
String line;
|
||||
int lineNumber = 1;
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
lineNumber++;
|
||||
if (line.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
GrilleTarifaire grille = parseCsvLine(line, headers, lineNumber);
|
||||
if (grille != null) {
|
||||
validerGrilleTarifaire(grille);
|
||||
GrilleTarifaire grilleSauvegardee = grilleTarifaireRepository.save(grille);
|
||||
grillesImportees.add(grilleSauvegardee);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur ligne {}: {}", lineNumber, e.getMessage());
|
||||
throw new IllegalArgumentException("Erreur ligne " + lineNumber + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Import CSV terminé - {} grilles importées", grillesImportees.size());
|
||||
return grillesImportees;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> importerDepuisExcel(MultipartFile file, String sheetName, String mode) throws IOException {
|
||||
log.info("Import Excel - Fichier: {}, Feuille: {}, Mode: {}", file.getOriginalFilename(), sheetName, mode);
|
||||
|
||||
// Pour cette version simplifiée, nous convertissons Excel vers CSV puis utilisons le parser CSV
|
||||
// Dans une implémentation complète, nous utiliserions Apache POI
|
||||
|
||||
throw new UnsupportedOperationException("L'import Excel n'est pas encore implémenté. Utilisez le format CSV.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> importerDepuisJson(List<GrilleTarifaire> grilles, String mode) {
|
||||
log.info("Import JSON - {} grilles, Mode: {}", grilles.size(), mode);
|
||||
|
||||
if ("REPLACE".equalsIgnoreCase(mode)) {
|
||||
log.info("Mode REPLACE - Suppression de toutes les grilles existantes");
|
||||
grilleTarifaireRepository.deleteAll();
|
||||
}
|
||||
|
||||
List<GrilleTarifaire> grillesImportees = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < grilles.size(); i++) {
|
||||
try {
|
||||
GrilleTarifaire grille = grilles.get(i);
|
||||
validerGrilleTarifaire(grille);
|
||||
|
||||
// Si la grille a un ID et existe déjà, mise à jour. Sinon, création.
|
||||
if (grille.getId() != null && grilleTarifaireRepository.existsById(grille.getId())) {
|
||||
log.debug("Mise à jour grille existante ID: {}", grille.getId());
|
||||
} else {
|
||||
grille.setId(null); // Force la création d'une nouvelle grille
|
||||
}
|
||||
|
||||
GrilleTarifaire grilleSauvegardee = grilleTarifaireRepository.save(grille);
|
||||
grillesImportees.add(grilleSauvegardee);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du traitement de la grille #{}: {}", i + 1, e.getMessage());
|
||||
throw new IllegalArgumentException("Erreur grille #" + (i + 1) + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Import JSON terminé - {} grilles importées", grillesImportees.size());
|
||||
return grillesImportees;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> listerGrilles(int page, int size, String transporteur, String paysOrigine, String paysDestination) {
|
||||
log.debug("Listing grilles - page: {}, size: {}, transporteur: {}, origine: {}, destination: {}",
|
||||
page, size, transporteur, paysOrigine, paysDestination);
|
||||
|
||||
// Pour cette implémentation simplifiée, nous récupérons toutes les grilles et filtrons
|
||||
// Dans une implémentation complète, nous utiliserions des requêtes JPA avec Specification
|
||||
|
||||
List<GrilleTarifaire> toutes = grilleTarifaireRepository.findAll();
|
||||
|
||||
// Application des filtres
|
||||
return toutes.stream()
|
||||
.filter(g -> transporteur == null || g.getTransporteur().toLowerCase().contains(transporteur.toLowerCase()))
|
||||
.filter(g -> paysOrigine == null || g.getOriginePays().equalsIgnoreCase(paysOrigine))
|
||||
.filter(g -> paysDestination == null || g.getDestinationPays().equalsIgnoreCase(paysDestination))
|
||||
.skip((long) page * size)
|
||||
.limit(size)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> validerFichier(MultipartFile file) throws IOException {
|
||||
log.info("Validation fichier - Nom: {}, Taille: {} bytes", file.getOriginalFilename(), file.getSize());
|
||||
|
||||
Map<String, Object> resultat = new HashMap<>();
|
||||
List<String> erreurs = new ArrayList<>();
|
||||
List<String> avertissements = new ArrayList<>();
|
||||
int lignesValides = 0;
|
||||
int lignesTotal = 0;
|
||||
|
||||
String filename = file.getOriginalFilename().toLowerCase();
|
||||
resultat.put("nomFichier", file.getOriginalFilename());
|
||||
resultat.put("tailleFichier", file.getSize());
|
||||
resultat.put("typeFichier", filename.endsWith(".csv") ? "CSV" : filename.endsWith(".xlsx") ? "Excel" : "Inconnu");
|
||||
|
||||
if (filename.endsWith(".csv")) {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), "UTF-8"))) {
|
||||
String headerLine = reader.readLine();
|
||||
lignesTotal++;
|
||||
|
||||
if (headerLine == null) {
|
||||
erreurs.add("Le fichier est vide");
|
||||
} else {
|
||||
String[] headers = headerLine.split(",");
|
||||
resultat.put("nombreColonnes", headers.length);
|
||||
resultat.put("colonnes", Arrays.asList(headers));
|
||||
|
||||
// Vérification des colonnes obligatoires
|
||||
List<String> colonnesObligatoires = Arrays.asList(
|
||||
"nomGrille", "transporteur", "typeService", "originePays", "destinationPays",
|
||||
"validiteDebut", "validiteFin"
|
||||
);
|
||||
|
||||
for (String colonne : colonnesObligatoires) {
|
||||
boolean trouve = false;
|
||||
for (String header : headers) {
|
||||
if (header.trim().equalsIgnoreCase(colonne)) {
|
||||
trouve = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!trouve) {
|
||||
erreurs.add("Colonne obligatoire manquante: " + colonne);
|
||||
}
|
||||
}
|
||||
|
||||
String line;
|
||||
int lineNumber = 1;
|
||||
|
||||
while ((line = reader.readLine()) != null && lineNumber <= 100) { // Limite pour la validation
|
||||
lineNumber++;
|
||||
lignesTotal++;
|
||||
|
||||
if (line.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
GrilleTarifaire grille = parseCsvLine(line, headers, lineNumber);
|
||||
if (grille != null) {
|
||||
validerGrilleTarifaire(grille);
|
||||
lignesValides++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
erreurs.add("Ligne " + lineNumber + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (lineNumber > 100) {
|
||||
avertissements.add("Validation limitée aux 100 premières lignes de données");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
erreurs.add("Format de fichier non supporté. Seuls les fichiers CSV sont actuellement supportés.");
|
||||
}
|
||||
|
||||
resultat.put("lignesTotal", lignesTotal);
|
||||
resultat.put("lignesValides", lignesValides);
|
||||
resultat.put("erreurs", erreurs);
|
||||
resultat.put("avertissements", avertissements);
|
||||
resultat.put("valide", erreurs.isEmpty());
|
||||
|
||||
return resultat;
|
||||
}
|
||||
|
||||
private GrilleTarifaire parseCsvLine(String line, String[] headers, int lineNumber) {
|
||||
String[] values = line.split(",", -1); // -1 pour conserver les valeurs vides
|
||||
|
||||
if (values.length != headers.length) {
|
||||
throw new IllegalArgumentException("Nombre de colonnes incorrect. Attendu: " + headers.length + ", trouvé: " + values.length);
|
||||
}
|
||||
|
||||
GrilleTarifaire grille = new GrilleTarifaire();
|
||||
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
String header = headers[i].trim();
|
||||
String value = values[i].trim();
|
||||
|
||||
if (value.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (header.toLowerCase()) {
|
||||
case "nomgrille":
|
||||
grille.setNomGrille(value);
|
||||
break;
|
||||
case "transporteur":
|
||||
grille.setTransporteur(value);
|
||||
break;
|
||||
case "typeservice":
|
||||
grille.setTypeService(GrilleTarifaire.TypeService.valueOf(value.toUpperCase()));
|
||||
break;
|
||||
case "originepays":
|
||||
grille.setOriginePays(value);
|
||||
break;
|
||||
case "destinationpays":
|
||||
grille.setDestinationPays(value);
|
||||
break;
|
||||
case "origineville":
|
||||
grille.setOrigineVille(value);
|
||||
break;
|
||||
case "destinationville":
|
||||
grille.setDestinationVille(value);
|
||||
break;
|
||||
case "validitedebut":
|
||||
grille.setValiditeDebut(LocalDate.parse(value, dateFormatter));
|
||||
break;
|
||||
case "validiteefin":
|
||||
grille.setValiditeFin(LocalDate.parse(value, dateFormatter));
|
||||
break;
|
||||
case "incoterm":
|
||||
grille.setIncoterm(value);
|
||||
break;
|
||||
case "modetransport":
|
||||
grille.setModeTransport(GrilleTarifaire.ModeTransport.valueOf(value.toUpperCase()));
|
||||
break;
|
||||
case "actif":
|
||||
grille.setActif(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "devisebase":
|
||||
grille.setDeviseBase(value);
|
||||
break;
|
||||
case "commentaires":
|
||||
grille.setCommentaires(value);
|
||||
break;
|
||||
// Pour les tarifs de fret et autres listes, nous aurions besoin d'un format plus complexe
|
||||
// Dans cette implémentation simplifiée, nous les ignorons
|
||||
default:
|
||||
log.debug("Colonne ignorée: {}", header);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Erreur dans la colonne '" + header + "': " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return grille;
|
||||
}
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.DemandeDevis;
|
||||
import com.dh7789dev.xpeditis.dto.app.GrilleTarifaire;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GrilleTarifaireServiceImpl implements GrilleTarifaireService {
|
||||
|
||||
private final GrilleTarifaireRepository grilleTarifaireRepository;
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> trouverGrillesApplicables(DemandeDevis demandeDevis) {
|
||||
log.info("Recherche des grilles tarifaires applicables pour {} -> {}",
|
||||
demandeDevis.getDepart().getPays(),
|
||||
demandeDevis.getArrivee().getPays());
|
||||
|
||||
LocalDate dateValidite = demandeDevis.getDateEnlevement() != null
|
||||
? demandeDevis.getDateEnlevement()
|
||||
: LocalDate.now();
|
||||
|
||||
List<GrilleTarifaire> grilles = grilleTarifaireRepository.findGrillesApplicables(
|
||||
demandeDevis.getTypeService(),
|
||||
demandeDevis.getDepart().getPays(),
|
||||
demandeDevis.getArrivee().getPays(),
|
||||
dateValidite
|
||||
);
|
||||
|
||||
// Filtrer par ville si spécifiée
|
||||
if (demandeDevis.getDepart().getVille() != null || demandeDevis.getArrivee().getVille() != null) {
|
||||
grilles = grilles.stream()
|
||||
.filter(g -> isVilleCompatible(g, demandeDevis))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Filtrer par incoterm si spécifié
|
||||
if (demandeDevis.getIncoterm() != null) {
|
||||
grilles = grilles.stream()
|
||||
.filter(g -> g.getIncoterm() == null || g.getIncoterm().equals(demandeDevis.getIncoterm()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
log.info("Trouvé {} grille(s) applicables", grilles.size());
|
||||
return grilles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrilleTarifaire sauvegarderGrille(GrilleTarifaire grilleTarifaire) {
|
||||
log.info("Sauvegarde de la grille tarifaire: {}", grilleTarifaire.getNomGrille());
|
||||
|
||||
validerGrilleTarifaire(grilleTarifaire);
|
||||
|
||||
return grilleTarifaireRepository.save(grilleTarifaire);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrilleTarifaire trouverParId(Long id) {
|
||||
log.debug("Recherche de la grille tarifaire avec l'ID: {}", id);
|
||||
|
||||
return grilleTarifaireRepository.findById(id)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void supprimerGrille(Long id) {
|
||||
log.info("Suppression de la grille tarifaire avec l'ID: {}", id);
|
||||
|
||||
if (!grilleTarifaireRepository.existsById(id)) {
|
||||
throw new IllegalArgumentException("Aucune grille tarifaire trouvée avec l'ID: " + id);
|
||||
}
|
||||
|
||||
grilleTarifaireRepository.deleteById(id);
|
||||
}
|
||||
|
||||
private boolean isVilleCompatible(GrilleTarifaire grille, DemandeDevis demande) {
|
||||
// Si la grille n'a pas de ville spécifiée, elle est compatible avec toutes les villes
|
||||
boolean origineCompatible = grille.getOrigineVille() == null ||
|
||||
grille.getOrigineVille().equalsIgnoreCase(demande.getDepart().getVille());
|
||||
|
||||
boolean destinationCompatible = grille.getDestinationVille() == null ||
|
||||
grille.getDestinationVille().equalsIgnoreCase(demande.getArrivee().getVille());
|
||||
|
||||
return origineCompatible && destinationCompatible;
|
||||
}
|
||||
|
||||
private void validerGrilleTarifaire(GrilleTarifaire grille) {
|
||||
if (grille == null) {
|
||||
throw new IllegalArgumentException("La grille tarifaire ne peut pas être nulle");
|
||||
}
|
||||
|
||||
if (grille.getNomGrille() == null || grille.getNomGrille().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le nom de la grille est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getTransporteur() == null || grille.getTransporteur().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le transporteur est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getTypeService() == null) {
|
||||
throw new IllegalArgumentException("Le type de service est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getOriginePays() == null || grille.getOriginePays().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le pays d'origine est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getDestinationPays() == null || grille.getDestinationPays().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le pays de destination est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getValiditeDebut() == null) {
|
||||
throw new IllegalArgumentException("La date de début de validité est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getValiditeFin() == null) {
|
||||
throw new IllegalArgumentException("La date de fin de validité est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getValiditeDebut().isAfter(grille.getValiditeFin())) {
|
||||
throw new IllegalArgumentException("La date de début doit être antérieure à la date de fin");
|
||||
}
|
||||
|
||||
// Validation des tarifs de fret
|
||||
if (grille.getTarifsFret() == null || grille.getTarifsFret().isEmpty()) {
|
||||
throw new IllegalArgumentException("Au moins un tarif de fret doit être défini");
|
||||
}
|
||||
|
||||
// Validation des codes pays (format ISO 3166-1 alpha-3)
|
||||
if (grille.getOriginePays().length() != 3) {
|
||||
throw new IllegalArgumentException("Le code pays d'origine doit être au format ISO 3166-1 alpha-3 (3 caractères)");
|
||||
}
|
||||
|
||||
if (grille.getDestinationPays().length() != 3) {
|
||||
throw new IllegalArgumentException("Le code pays de destination doit être au format ISO 3166-1 alpha-3 (3 caractères)");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> importerDepuisCsv(MultipartFile file, String mode) throws IOException {
|
||||
log.info("Import CSV - Fichier: {}, Mode: {}", file.getOriginalFilename(), mode);
|
||||
|
||||
if ("REPLACE".equalsIgnoreCase(mode)) {
|
||||
log.info("Mode REPLACE - Suppression de toutes les grilles existantes");
|
||||
grilleTarifaireRepository.deleteAll();
|
||||
}
|
||||
|
||||
List<GrilleTarifaire> grillesImportees = new ArrayList<>();
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), "UTF-8"))) {
|
||||
String headerLine = reader.readLine();
|
||||
if (headerLine == null) {
|
||||
throw new IllegalArgumentException("Le fichier CSV est vide");
|
||||
}
|
||||
|
||||
String[] headers = headerLine.split(",");
|
||||
log.debug("En-têtes CSV: {}", Arrays.toString(headers));
|
||||
|
||||
String line;
|
||||
int lineNumber = 1;
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
lineNumber++;
|
||||
if (line.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
GrilleTarifaire grille = parseCsvLine(line, headers, lineNumber);
|
||||
if (grille != null) {
|
||||
validerGrilleTarifaire(grille);
|
||||
GrilleTarifaire grilleSauvegardee = grilleTarifaireRepository.save(grille);
|
||||
grillesImportees.add(grilleSauvegardee);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur ligne {}: {}", lineNumber, e.getMessage());
|
||||
throw new IllegalArgumentException("Erreur ligne " + lineNumber + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Import CSV terminé - {} grilles importées", grillesImportees.size());
|
||||
return grillesImportees;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> importerDepuisExcel(MultipartFile file, String sheetName, String mode) throws IOException {
|
||||
log.info("Import Excel - Fichier: {}, Feuille: {}, Mode: {}", file.getOriginalFilename(), sheetName, mode);
|
||||
|
||||
// Pour cette version simplifiée, nous convertissons Excel vers CSV puis utilisons le parser CSV
|
||||
// Dans une implémentation complète, nous utiliserions Apache POI
|
||||
|
||||
throw new UnsupportedOperationException("L'import Excel n'est pas encore implémenté. Utilisez le format CSV.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> importerDepuisJson(List<GrilleTarifaire> grilles, String mode) {
|
||||
log.info("Import JSON - {} grilles, Mode: {}", grilles.size(), mode);
|
||||
|
||||
if ("REPLACE".equalsIgnoreCase(mode)) {
|
||||
log.info("Mode REPLACE - Suppression de toutes les grilles existantes");
|
||||
grilleTarifaireRepository.deleteAll();
|
||||
}
|
||||
|
||||
List<GrilleTarifaire> grillesImportees = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < grilles.size(); i++) {
|
||||
try {
|
||||
GrilleTarifaire grille = grilles.get(i);
|
||||
validerGrilleTarifaire(grille);
|
||||
|
||||
// Si la grille a un ID et existe déjà, mise à jour. Sinon, création.
|
||||
if (grille.getId() != null && grilleTarifaireRepository.existsById(grille.getId())) {
|
||||
log.debug("Mise à jour grille existante ID: {}", grille.getId());
|
||||
} else {
|
||||
grille.setId(null); // Force la création d'une nouvelle grille
|
||||
}
|
||||
|
||||
GrilleTarifaire grilleSauvegardee = grilleTarifaireRepository.save(grille);
|
||||
grillesImportees.add(grilleSauvegardee);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du traitement de la grille #{}: {}", i + 1, e.getMessage());
|
||||
throw new IllegalArgumentException("Erreur grille #" + (i + 1) + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Import JSON terminé - {} grilles importées", grillesImportees.size());
|
||||
return grillesImportees;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> listerGrilles(int page, int size, String transporteur, String paysOrigine, String paysDestination) {
|
||||
log.debug("Listing grilles - page: {}, size: {}, transporteur: {}, origine: {}, destination: {}",
|
||||
page, size, transporteur, paysOrigine, paysDestination);
|
||||
|
||||
// Pour cette implémentation simplifiée, nous récupérons toutes les grilles et filtrons
|
||||
// Dans une implémentation complète, nous utiliserions des requêtes JPA avec Specification
|
||||
|
||||
List<GrilleTarifaire> toutes = grilleTarifaireRepository.findAll();
|
||||
|
||||
// Application des filtres
|
||||
return toutes.stream()
|
||||
.filter(g -> transporteur == null || g.getTransporteur().toLowerCase().contains(transporteur.toLowerCase()))
|
||||
.filter(g -> paysOrigine == null || g.getOriginePays().equalsIgnoreCase(paysOrigine))
|
||||
.filter(g -> paysDestination == null || g.getDestinationPays().equalsIgnoreCase(paysDestination))
|
||||
.skip((long) page * size)
|
||||
.limit(size)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> validerFichier(MultipartFile file) throws IOException {
|
||||
log.info("Validation fichier - Nom: {}, Taille: {} bytes", file.getOriginalFilename(), file.getSize());
|
||||
|
||||
Map<String, Object> resultat = new HashMap<>();
|
||||
List<String> erreurs = new ArrayList<>();
|
||||
List<String> avertissements = new ArrayList<>();
|
||||
int lignesValides = 0;
|
||||
int lignesTotal = 0;
|
||||
|
||||
String filename = file.getOriginalFilename().toLowerCase();
|
||||
resultat.put("nomFichier", file.getOriginalFilename());
|
||||
resultat.put("tailleFichier", file.getSize());
|
||||
resultat.put("typeFichier", filename.endsWith(".csv") ? "CSV" : filename.endsWith(".xlsx") ? "Excel" : "Inconnu");
|
||||
|
||||
if (filename.endsWith(".csv")) {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), "UTF-8"))) {
|
||||
String headerLine = reader.readLine();
|
||||
lignesTotal++;
|
||||
|
||||
if (headerLine == null) {
|
||||
erreurs.add("Le fichier est vide");
|
||||
} else {
|
||||
String[] headers = headerLine.split(",");
|
||||
resultat.put("nombreColonnes", headers.length);
|
||||
resultat.put("colonnes", Arrays.asList(headers));
|
||||
|
||||
// Vérification des colonnes obligatoires
|
||||
List<String> colonnesObligatoires = Arrays.asList(
|
||||
"nomGrille", "transporteur", "typeService", "originePays", "destinationPays",
|
||||
"validiteDebut", "validiteFin"
|
||||
);
|
||||
|
||||
for (String colonne : colonnesObligatoires) {
|
||||
boolean trouve = false;
|
||||
for (String header : headers) {
|
||||
if (header.trim().equalsIgnoreCase(colonne)) {
|
||||
trouve = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!trouve) {
|
||||
erreurs.add("Colonne obligatoire manquante: " + colonne);
|
||||
}
|
||||
}
|
||||
|
||||
String line;
|
||||
int lineNumber = 1;
|
||||
|
||||
while ((line = reader.readLine()) != null && lineNumber <= 100) { // Limite pour la validation
|
||||
lineNumber++;
|
||||
lignesTotal++;
|
||||
|
||||
if (line.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
GrilleTarifaire grille = parseCsvLine(line, headers, lineNumber);
|
||||
if (grille != null) {
|
||||
validerGrilleTarifaire(grille);
|
||||
lignesValides++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
erreurs.add("Ligne " + lineNumber + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (lineNumber > 100) {
|
||||
avertissements.add("Validation limitée aux 100 premières lignes de données");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
erreurs.add("Format de fichier non supporté. Seuls les fichiers CSV sont actuellement supportés.");
|
||||
}
|
||||
|
||||
resultat.put("lignesTotal", lignesTotal);
|
||||
resultat.put("lignesValides", lignesValides);
|
||||
resultat.put("erreurs", erreurs);
|
||||
resultat.put("avertissements", avertissements);
|
||||
resultat.put("valide", erreurs.isEmpty());
|
||||
|
||||
return resultat;
|
||||
}
|
||||
|
||||
private GrilleTarifaire parseCsvLine(String line, String[] headers, int lineNumber) {
|
||||
String[] values = line.split(",", -1); // -1 pour conserver les valeurs vides
|
||||
|
||||
if (values.length != headers.length) {
|
||||
throw new IllegalArgumentException("Nombre de colonnes incorrect. Attendu: " + headers.length + ", trouvé: " + values.length);
|
||||
}
|
||||
|
||||
GrilleTarifaire grille = new GrilleTarifaire();
|
||||
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
String header = headers[i].trim();
|
||||
String value = values[i].trim();
|
||||
|
||||
if (value.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (header.toLowerCase()) {
|
||||
case "nomgrille":
|
||||
grille.setNomGrille(value);
|
||||
break;
|
||||
case "transporteur":
|
||||
grille.setTransporteur(value);
|
||||
break;
|
||||
case "typeservice":
|
||||
grille.setTypeService(GrilleTarifaire.TypeService.valueOf(value.toUpperCase()));
|
||||
break;
|
||||
case "originepays":
|
||||
grille.setOriginePays(value);
|
||||
break;
|
||||
case "destinationpays":
|
||||
grille.setDestinationPays(value);
|
||||
break;
|
||||
case "origineville":
|
||||
grille.setOrigineVille(value);
|
||||
break;
|
||||
case "destinationville":
|
||||
grille.setDestinationVille(value);
|
||||
break;
|
||||
case "validitedebut":
|
||||
grille.setValiditeDebut(LocalDate.parse(value, dateFormatter));
|
||||
break;
|
||||
case "validiteefin":
|
||||
grille.setValiditeFin(LocalDate.parse(value, dateFormatter));
|
||||
break;
|
||||
case "incoterm":
|
||||
grille.setIncoterm(value);
|
||||
break;
|
||||
case "modetransport":
|
||||
grille.setModeTransport(GrilleTarifaire.ModeTransport.valueOf(value.toUpperCase()));
|
||||
break;
|
||||
case "actif":
|
||||
grille.setActif(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "devisebase":
|
||||
grille.setDeviseBase(value);
|
||||
break;
|
||||
case "commentaires":
|
||||
grille.setCommentaires(value);
|
||||
break;
|
||||
// Pour les tarifs de fret et autres listes, nous aurions besoin d'un format plus complexe
|
||||
// Dans cette implémentation simplifiée, nous les ignorons
|
||||
default:
|
||||
log.debug("Colonne ignorée: {}", header);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Erreur dans la colonne '" + header + "': " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return grille;
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,30 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.Company;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.dto.app.License;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.dto.app.LicenseStatus;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.dto.app.PlanFeature;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.Set;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class LicenseServiceImpl implements LicenseService {
|
||||
|
||||
|
||||
@ -1,56 +1,61 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
@Service
|
||||
public class NlsServiceImpl implements NlsService {
|
||||
|
||||
private final NlsRepository nlsRepository;
|
||||
|
||||
@Autowired
|
||||
public NlsServiceImpl(NlsRepository nlsRepository) {
|
||||
this.nlsRepository = nlsRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key nls message key
|
||||
* @return the message corresponding to the key translated into the desired language
|
||||
*/
|
||||
@Override
|
||||
public String getMessage(String key) {
|
||||
return nlsRepository.getMessage(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key nls message key
|
||||
* @param parameters parameters to use in the message
|
||||
* @return the message corresponding to the key translated into the desired language
|
||||
*/
|
||||
@Override
|
||||
public String getMessage(String key, Object[] parameters) {
|
||||
String message = nlsRepository.getMessage(key, parameters);
|
||||
// escape simple quotes: double them
|
||||
message = message.replace("'", "''").replace("{", "'{");
|
||||
// format message
|
||||
return MessageFormat.format(message, parameters);
|
||||
}
|
||||
|
||||
private Locale getLocale(final Locale locale) {
|
||||
Locale locale1;
|
||||
if (locale != null) {
|
||||
// hack for simplified chinese
|
||||
if (locale.getLanguage().equals("zh") && locale.getCountry().isEmpty()) {
|
||||
locale1 = Locale.SIMPLIFIED_CHINESE;
|
||||
} else {
|
||||
locale1 = locale;
|
||||
}
|
||||
} else {
|
||||
locale1 = Locale.getDefault();
|
||||
}
|
||||
return locale1;
|
||||
}
|
||||
}
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.Locale;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class NlsServiceImpl implements NlsService {
|
||||
|
||||
private final NlsRepository nlsRepository;
|
||||
|
||||
@Autowired
|
||||
public NlsServiceImpl(NlsRepository nlsRepository) {
|
||||
this.nlsRepository = nlsRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key nls message key
|
||||
* @return the message corresponding to the key translated into the desired language
|
||||
*/
|
||||
@Override
|
||||
public String getMessage(String key) {
|
||||
return nlsRepository.getMessage(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key nls message key
|
||||
* @param parameters parameters to use in the message
|
||||
* @return the message corresponding to the key translated into the desired language
|
||||
*/
|
||||
@Override
|
||||
public String getMessage(String key, Object[] parameters) {
|
||||
String message = nlsRepository.getMessage(key, parameters);
|
||||
// escape simple quotes: double them
|
||||
message = message.replace("'", "''").replace("{", "'{");
|
||||
// format message
|
||||
return MessageFormat.format(message, parameters);
|
||||
}
|
||||
|
||||
private Locale getLocale(final Locale locale) {
|
||||
Locale locale1;
|
||||
if (locale != null) {
|
||||
// hack for simplified chinese
|
||||
if (locale.getLanguage().equals("zh") && locale.getCountry().isEmpty()) {
|
||||
locale1 = Locale.SIMPLIFIED_CHINESE;
|
||||
} else {
|
||||
locale1 = locale;
|
||||
}
|
||||
} else {
|
||||
locale1 = Locale.getDefault();
|
||||
}
|
||||
return locale1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class NotificationServiceImpl implements NotificationService {
|
||||
}
|
||||
|
||||
@ -1,171 +1,188 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.Plan;
|
||||
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||
import com.dh7789dev.xpeditis.dto.app.BillingCycle;
|
||||
import com.dh7789dev.xpeditis.dto.valueobject.Money;
|
||||
import com.dh7789dev.xpeditis.port.out.PlanRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.ArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implémentation du service de gestion des plans
|
||||
*/
|
||||
@Service
|
||||
public class PlanServiceImpl implements PlanService {
|
||||
|
||||
private final PlanRepository planRepository;
|
||||
|
||||
public PlanServiceImpl(PlanRepository planRepository) {
|
||||
this.planRepository = planRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plan> getAllActivePlans() {
|
||||
return planRepository.findAllByIsActiveTrue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plan getPlanById(UUID planId) {
|
||||
return planRepository.findById(planId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Plan non trouvé: " + planId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plan getPlanByType(LicenseType type) {
|
||||
return planRepository.findByTypeAndIsActiveTrue(type)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Plan non trouvé pour le type: " + type));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plan getPlanByStripePriceId(String stripePriceId) {
|
||||
return planRepository.findByStripePriceId(stripePriceId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Plan non trouvé pour l'ID Stripe: " + stripePriceId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money calculateProrata(Plan currentPlan, Plan newPlan, BillingCycle cycle, int daysRemaining) {
|
||||
if (currentPlan.equals(newPlan)) {
|
||||
return Money.zeroEuros();
|
||||
}
|
||||
|
||||
BigDecimal currentPrice = currentPlan.getPriceForCycle(cycle);
|
||||
BigDecimal newPrice = newPlan.getPriceForCycle(cycle);
|
||||
|
||||
int totalDaysInCycle = cycle.getMonths() * 30; // Approximation
|
||||
|
||||
// Calcul du crédit pour la période restante du plan actuel
|
||||
BigDecimal dailyCurrentRate = currentPrice.divide(BigDecimal.valueOf(totalDaysInCycle), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal credit = dailyCurrentRate.multiply(BigDecimal.valueOf(daysRemaining));
|
||||
|
||||
// Calcul du coût pour la période restante du nouveau plan
|
||||
BigDecimal dailyNewRate = newPrice.divide(BigDecimal.valueOf(totalDaysInCycle), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal newCost = dailyNewRate.multiply(BigDecimal.valueOf(daysRemaining));
|
||||
|
||||
// Montant à payer = nouveau coût - crédit
|
||||
BigDecimal amountDue = newCost.subtract(credit);
|
||||
|
||||
return Money.euros(amountDue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plan> findSuitablePlansForUserCount(int userCount) {
|
||||
return planRepository.findSuitableForUserCount(userCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlanComparison comparePlans(Plan plan1, Plan plan2) {
|
||||
Set<String> features1 = plan1.getFeatures() != null ? plan1.getFeatures() : Set.of();
|
||||
Set<String> features2 = plan2.getFeatures() != null ? plan2.getFeatures() : Set.of();
|
||||
|
||||
// Fonctionnalités ajoutées (dans plan2 mais pas dans plan1)
|
||||
List<String> addedFeatures = features2.stream()
|
||||
.filter(feature -> !features1.contains(feature))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Fonctionnalités supprimées (dans plan1 mais pas dans plan2)
|
||||
List<String> removedFeatures = features1.stream()
|
||||
.filter(feature -> !features2.contains(feature))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Différence de prix (mensuel)
|
||||
BigDecimal priceDiff = plan2.getMonthlyPrice().subtract(plan1.getMonthlyPrice());
|
||||
Money priceDifference = Money.euros(priceDiff);
|
||||
|
||||
// Déterminer si c'est un upgrade
|
||||
boolean isUpgrade = isUpgrade(plan1, plan2);
|
||||
|
||||
return new PlanComparison(plan1, plan2, addedFeatures, removedFeatures, priceDifference, isUpgrade);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canChangePlan(Plan currentPlan, Plan targetPlan, int currentUserCount) {
|
||||
// Vérifier si le plan cible peut supporter le nombre d'utilisateurs actuel
|
||||
if (targetPlan.hasUserLimit() && targetPlan.getMaxUsers() != null
|
||||
&& currentUserCount > targetPlan.getMaxUsers()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérifier si c'est un downgrade valide (pas de perte de fonctionnalités critiques utilisées)
|
||||
if (!isUpgrade(currentPlan, targetPlan)) {
|
||||
// Pour un downgrade, on pourrait vérifier l'utilisation des fonctionnalités
|
||||
// Ici on autorise tous les downgrades pour simplifier
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plan getRecommendedPlan(int userCount, List<String> requiredFeatures) {
|
||||
List<Plan> allPlans = getAllActivePlans();
|
||||
|
||||
return allPlans.stream()
|
||||
.filter(plan -> {
|
||||
// Le plan doit supporter le nombre d'utilisateurs
|
||||
if (plan.hasUserLimit() && plan.getMaxUsers() != null && userCount > plan.getMaxUsers()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Le plan doit inclure toutes les fonctionnalités requises
|
||||
Set<String> planFeatures = plan.getFeatures() != null ? plan.getFeatures() : Set.of();
|
||||
return planFeatures.containsAll(requiredFeatures);
|
||||
})
|
||||
.min((p1, p2) -> {
|
||||
// Prendre le plan le moins cher qui satisfait les critères
|
||||
return p1.getMonthlyPrice().compareTo(p2.getMonthlyPrice());
|
||||
})
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine si le changement de plan1 vers plan2 est un upgrade
|
||||
*/
|
||||
private boolean isUpgrade(Plan plan1, Plan plan2) {
|
||||
// Basé sur l'ordre des types de licence
|
||||
int order1 = getLicenseTypeOrder(plan1.getType());
|
||||
int order2 = getLicenseTypeOrder(plan2.getType());
|
||||
|
||||
return order2 > order1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'ordre du type de licence pour déterminer les upgrades
|
||||
*/
|
||||
private int getLicenseTypeOrder(LicenseType type) {
|
||||
switch (type) {
|
||||
case TRIAL: return 0;
|
||||
case BASIC: return 1;
|
||||
case PREMIUM: return 2;
|
||||
case ENTERPRISE: return 3;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.Plan;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.dto.app.BillingCycle;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.dto.valueobject.Money;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.port.out.PlanRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.math.RoundingMode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.Set;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.ArrayList;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Implémentation du service de gestion des plans
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PlanServiceImpl implements PlanService {
|
||||
|
||||
private final PlanRepository planRepository;
|
||||
|
||||
public PlanServiceImpl(PlanRepository planRepository) {
|
||||
this.planRepository = planRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plan> getAllActivePlans() {
|
||||
return planRepository.findAllByIsActiveTrue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plan getPlanById(UUID planId) {
|
||||
return planRepository.findById(planId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Plan non trouvé: " + planId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plan getPlanByType(LicenseType type) {
|
||||
List<Plan> plans = planRepository.findByTypeAndIsActiveTrue(type);
|
||||
if (plans.isEmpty()) {
|
||||
throw new IllegalArgumentException("Plan non trouvé pour le type: " + type);
|
||||
}
|
||||
return plans.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plan getPlanByStripePriceId(String stripePriceId) {
|
||||
return planRepository.findByStripePriceId(stripePriceId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Plan non trouvé pour l'ID Stripe: " + stripePriceId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money calculateProrata(Plan currentPlan, Plan newPlan, BillingCycle cycle, int daysRemaining) {
|
||||
if (currentPlan.equals(newPlan)) {
|
||||
return Money.zeroEuros();
|
||||
}
|
||||
|
||||
BigDecimal currentPrice = currentPlan.getPriceForCycle(cycle);
|
||||
BigDecimal newPrice = newPlan.getPriceForCycle(cycle);
|
||||
|
||||
int totalDaysInCycle = cycle.getMonths() * 30; // Approximation
|
||||
|
||||
// Calcul du crédit pour la période restante du plan actuel
|
||||
BigDecimal dailyCurrentRate = currentPrice.divide(BigDecimal.valueOf(totalDaysInCycle), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal credit = dailyCurrentRate.multiply(BigDecimal.valueOf(daysRemaining));
|
||||
|
||||
// Calcul du coût pour la période restante du nouveau plan
|
||||
BigDecimal dailyNewRate = newPrice.divide(BigDecimal.valueOf(totalDaysInCycle), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal newCost = dailyNewRate.multiply(BigDecimal.valueOf(daysRemaining));
|
||||
|
||||
// Montant à payer = nouveau coût - crédit
|
||||
BigDecimal amountDue = newCost.subtract(credit);
|
||||
|
||||
return Money.euros(amountDue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plan> findSuitablePlansForUserCount(int userCount) {
|
||||
return planRepository.findSuitableForUserCount(userCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlanComparison comparePlans(Plan plan1, Plan plan2) {
|
||||
Set<String> features1 = plan1.getFeatures() != null ? plan1.getFeatures() : Set.of();
|
||||
Set<String> features2 = plan2.getFeatures() != null ? plan2.getFeatures() : Set.of();
|
||||
|
||||
// Fonctionnalités ajoutées (dans plan2 mais pas dans plan1)
|
||||
List<String> addedFeatures = features2.stream()
|
||||
.filter(feature -> !features1.contains(feature))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Fonctionnalités supprimées (dans plan1 mais pas dans plan2)
|
||||
List<String> removedFeatures = features1.stream()
|
||||
.filter(feature -> !features2.contains(feature))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Différence de prix (mensuel)
|
||||
BigDecimal priceDiff = plan2.getMonthlyPrice().subtract(plan1.getMonthlyPrice());
|
||||
Money priceDifference = Money.euros(priceDiff);
|
||||
|
||||
// Déterminer si c'est un upgrade
|
||||
boolean isUpgrade = isUpgrade(plan1, plan2);
|
||||
|
||||
return new PlanComparison(plan1, plan2, addedFeatures, removedFeatures, priceDifference, isUpgrade);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canChangePlan(Plan currentPlan, Plan targetPlan, int currentUserCount) {
|
||||
// Vérifier si le plan cible peut supporter le nombre d'utilisateurs actuel
|
||||
if (targetPlan.hasUserLimit() && targetPlan.getMaxUsers() != null
|
||||
&& currentUserCount > targetPlan.getMaxUsers()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérifier si c'est un downgrade valide (pas de perte de fonctionnalités critiques utilisées)
|
||||
if (!isUpgrade(currentPlan, targetPlan)) {
|
||||
// Pour un downgrade, on pourrait vérifier l'utilisation des fonctionnalités
|
||||
// Ici on autorise tous les downgrades pour simplifier
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plan getRecommendedPlan(int userCount, List<String> requiredFeatures) {
|
||||
List<Plan> allPlans = getAllActivePlans();
|
||||
|
||||
return allPlans.stream()
|
||||
.filter(plan -> {
|
||||
// Le plan doit supporter le nombre d'utilisateurs
|
||||
if (plan.hasUserLimit() && plan.getMaxUsers() != null && userCount > plan.getMaxUsers()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Le plan doit inclure toutes les fonctionnalités requises
|
||||
Set<String> planFeatures = plan.getFeatures() != null ? plan.getFeatures() : Set.of();
|
||||
return planFeatures.containsAll(requiredFeatures);
|
||||
})
|
||||
.min((p1, p2) -> {
|
||||
// Prendre le plan le moins cher qui satisfait les critères
|
||||
return p1.getMonthlyPrice().compareTo(p2.getMonthlyPrice());
|
||||
})
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine si le changement de plan1 vers plan2 est un upgrade
|
||||
*/
|
||||
private boolean isUpgrade(Plan plan1, Plan plan2) {
|
||||
// Basé sur l'ordre des types de licence
|
||||
int order1 = getLicenseTypeOrder(plan1.getType());
|
||||
int order2 = getLicenseTypeOrder(plan2.getType());
|
||||
|
||||
return order2 > order1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'ordre du type de licence pour déterminer les upgrades
|
||||
*/
|
||||
private int getLicenseTypeOrder(LicenseType type) {
|
||||
switch (type) {
|
||||
case TRIAL: return 0;
|
||||
case BASIC: return 1;
|
||||
case PREMIUM: return 2;
|
||||
case ENTERPRISE: return 3;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class QuoteDetailsServiceImpl implements QuoteDetailsService {
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class QuoteServiceImpl implements QuoteService {
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ShipmentTrackingServiceImpl implements ShipmentTrackingService {
|
||||
}
|
||||
|
||||
@ -1,263 +1,331 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.*;
|
||||
import com.dh7789dev.xpeditis.port.out.SubscriptionRepository;
|
||||
import com.dh7789dev.xpeditis.port.out.InvoiceRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Implémentation du service de gestion des abonnements
|
||||
*/
|
||||
@Service
|
||||
public class SubscriptionServiceImpl implements SubscriptionService {
|
||||
|
||||
private final SubscriptionRepository subscriptionRepository;
|
||||
private final InvoiceRepository invoiceRepository;
|
||||
private final LicenseService licenseService;
|
||||
private static final int GRACE_PERIOD_DAYS = 7;
|
||||
|
||||
public SubscriptionServiceImpl(
|
||||
SubscriptionRepository subscriptionRepository,
|
||||
InvoiceRepository invoiceRepository,
|
||||
LicenseService licenseService
|
||||
) {
|
||||
this.subscriptionRepository = subscriptionRepository;
|
||||
this.invoiceRepository = invoiceRepository;
|
||||
this.licenseService = licenseService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Subscription createSubscription(UUID companyId, Plan plan, BillingCycle billingCycle) {
|
||||
// Vérifier qu'il n'y a pas déjà un abonnement actif
|
||||
Subscription existingSubscription = getActiveSubscription(companyId);
|
||||
if (existingSubscription != null) {
|
||||
throw new IllegalStateException("Une entreprise ne peut avoir qu'un seul abonnement actif");
|
||||
}
|
||||
|
||||
// Créer l'abonnement
|
||||
Subscription subscription = Subscription.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.status(SubscriptionStatus.INCOMPLETE) // En attente de la création Stripe
|
||||
.billingCycle(billingCycle)
|
||||
.currentPeriodStart(LocalDateTime.now())
|
||||
.currentPeriodEnd(LocalDateTime.now().plusDays(billingCycle.getMonths() * 30L))
|
||||
.cancelAtPeriodEnd(false)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// Calculer la prochaine date de facturation
|
||||
subscription.setNextBillingDate(subscription.getCurrentPeriodEnd());
|
||||
|
||||
return subscriptionRepository.save(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Subscription updateSubscriptionFromStripe(String stripeSubscriptionId, SubscriptionStatus newStatus) {
|
||||
Subscription subscription = subscriptionRepository.findByStripeSubscriptionId(stripeSubscriptionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Abonnement Stripe non trouvé: " + stripeSubscriptionId));
|
||||
|
||||
SubscriptionStatus oldStatus = subscription.getStatus();
|
||||
subscription.updateStatus(newStatus);
|
||||
|
||||
// Gérer les transitions d'état
|
||||
handleStatusTransition(subscription, oldStatus, newStatus);
|
||||
|
||||
return subscriptionRepository.save(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Subscription changePlan(UUID companyId, Plan newPlan, boolean immediate) {
|
||||
Subscription subscription = getActiveSubscription(companyId);
|
||||
if (subscription == null) {
|
||||
throw new IllegalArgumentException("Aucun abonnement actif trouvé pour cette entreprise");
|
||||
}
|
||||
|
||||
// Mettre à jour l'abonnement
|
||||
subscription.setStripePriceId(newPlan.getStripePriceIdForCycle(subscription.getBillingCycle()));
|
||||
subscription.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
// Si c'est immédiat, mettre à jour la licence aussi
|
||||
if (immediate && subscription.getLicense() != null) {
|
||||
licenseService.upgradeLicense(companyId, newPlan.getType());
|
||||
}
|
||||
|
||||
return subscriptionRepository.save(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Subscription cancelSubscription(UUID companyId, boolean immediate, String reason) {
|
||||
Subscription subscription = getActiveSubscription(companyId);
|
||||
if (subscription == null) {
|
||||
throw new IllegalArgumentException("Aucun abonnement actif trouvé pour cette entreprise");
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
subscription.updateStatus(SubscriptionStatus.CANCELED);
|
||||
// Suspendre la licence immédiatement
|
||||
if (subscription.getLicense() != null) {
|
||||
licenseService.deactivateLicense(subscription.getLicense().getId());
|
||||
}
|
||||
} else {
|
||||
// Programmer l'annulation à la fin de la période
|
||||
subscription.scheduleForCancellation();
|
||||
}
|
||||
|
||||
return subscriptionRepository.save(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Subscription reactivateSubscription(UUID companyId) {
|
||||
// Chercher un abonnement annulé ou suspendu
|
||||
List<Subscription> subscriptions = subscriptionRepository.findByCompanyId(companyId);
|
||||
Subscription subscription = subscriptions.stream()
|
||||
.filter(s -> s.canBeReactivated())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Aucun abonnement réactivable trouvé"));
|
||||
|
||||
subscription.updateStatus(SubscriptionStatus.ACTIVE);
|
||||
subscription.unscheduleForCancellation();
|
||||
|
||||
// Réactiver la licence
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().reactivate();
|
||||
}
|
||||
|
||||
return subscriptionRepository.save(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscription getActiveSubscription(UUID companyId) {
|
||||
return subscriptionRepository.findActiveByCompanyId(companyId)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void handlePaymentFailure(String stripeSubscriptionId, String reason) {
|
||||
Subscription subscription = subscriptionRepository.findByStripeSubscriptionId(stripeSubscriptionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Abonnement non trouvé"));
|
||||
|
||||
// Mettre en statut PAST_DUE
|
||||
subscription.updateStatus(SubscriptionStatus.PAST_DUE);
|
||||
|
||||
// Démarrer la période de grâce pour la licence
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().startGracePeriod(GRACE_PERIOD_DAYS);
|
||||
}
|
||||
|
||||
subscriptionRepository.save(subscription);
|
||||
|
||||
// TODO: Envoyer notification d'échec de paiement
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void handlePaymentSuccess(String stripeSubscriptionId, String stripeInvoiceId) {
|
||||
Subscription subscription = subscriptionRepository.findByStripeSubscriptionId(stripeSubscriptionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Abonnement non trouvé"));
|
||||
|
||||
// Remettre en statut actif si nécessaire
|
||||
if (subscription.getStatus() == SubscriptionStatus.PAST_DUE) {
|
||||
subscription.updateStatus(SubscriptionStatus.ACTIVE);
|
||||
|
||||
// Réactiver la licence
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().reactivate();
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour les dates de période
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
subscription.setCurrentPeriodStart(now);
|
||||
subscription.setCurrentPeriodEnd(now.plusDays(subscription.getBillingCycle().getMonths() * 30L));
|
||||
subscription.setNextBillingDate(subscription.getCurrentPeriodEnd());
|
||||
|
||||
subscriptionRepository.save(subscription);
|
||||
|
||||
// TODO: Envoyer notification de paiement réussi
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> getSubscriptionsRequiringAttention() {
|
||||
List<Subscription> pastDue = subscriptionRepository.findByStatus(SubscriptionStatus.PAST_DUE);
|
||||
List<Subscription> unpaid = subscriptionRepository.findByStatus(SubscriptionStatus.UNPAID);
|
||||
List<Subscription> incomplete = subscriptionRepository.findByStatus(SubscriptionStatus.INCOMPLETE);
|
||||
|
||||
pastDue.addAll(unpaid);
|
||||
pastDue.addAll(incomplete);
|
||||
|
||||
return pastDue;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void startGracePeriod(UUID subscriptionId) {
|
||||
Subscription subscription = subscriptionRepository.findById(subscriptionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Abonnement non trouvé"));
|
||||
|
||||
subscription.updateStatus(SubscriptionStatus.PAST_DUE);
|
||||
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().startGracePeriod(GRACE_PERIOD_DAYS);
|
||||
}
|
||||
|
||||
subscriptionRepository.save(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void suspendUnpaidSubscriptions() {
|
||||
// Récupérer les abonnements en période de grâce expirée
|
||||
List<Subscription> gracePeriodExpired = subscriptionRepository.findInGracePeriod().stream()
|
||||
.filter(subscription -> subscription.getLicense() != null
|
||||
&& subscription.getLicense().getDaysRemainingInGracePeriod() <= 0)
|
||||
.toList();
|
||||
|
||||
for (Subscription subscription : gracePeriodExpired) {
|
||||
subscription.updateStatus(SubscriptionStatus.UNPAID);
|
||||
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().suspend();
|
||||
}
|
||||
|
||||
subscriptionRepository.save(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère les transitions d'état des abonnements
|
||||
*/
|
||||
private void handleStatusTransition(Subscription subscription, SubscriptionStatus oldStatus, SubscriptionStatus newStatus) {
|
||||
// Transition vers ACTIVE
|
||||
if (newStatus == SubscriptionStatus.ACTIVE && oldStatus != SubscriptionStatus.ACTIVE) {
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().reactivate();
|
||||
}
|
||||
}
|
||||
|
||||
// Transition vers CANCELED
|
||||
if (newStatus == SubscriptionStatus.CANCELED) {
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().suspend();
|
||||
}
|
||||
}
|
||||
|
||||
// Transition vers PAST_DUE
|
||||
if (newStatus == SubscriptionStatus.PAST_DUE) {
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().startGracePeriod(GRACE_PERIOD_DAYS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.port.out.SubscriptionRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.port.out.InvoiceRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.port.in.SubscriptionService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Implémentation du service de gestion des abonnements
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SubscriptionServiceImpl implements SubscriptionService {
|
||||
|
||||
private final SubscriptionRepository subscriptionRepository;
|
||||
private final InvoiceRepository invoiceRepository;
|
||||
private final LicenseService licenseService;
|
||||
private static final int GRACE_PERIOD_DAYS = 7;
|
||||
|
||||
public SubscriptionServiceImpl(
|
||||
SubscriptionRepository subscriptionRepository,
|
||||
InvoiceRepository invoiceRepository,
|
||||
LicenseService licenseService
|
||||
) {
|
||||
this.subscriptionRepository = subscriptionRepository;
|
||||
this.invoiceRepository = invoiceRepository;
|
||||
this.licenseService = licenseService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Subscription createSubscription(UUID companyId, Plan plan, BillingCycle billingCycle) {
|
||||
// Vérifier qu'il n'y a pas déjà un abonnement actif
|
||||
Subscription existingSubscription = getActiveSubscription(companyId);
|
||||
if (existingSubscription != null) {
|
||||
throw new IllegalStateException("Une entreprise ne peut avoir qu'un seul abonnement actif");
|
||||
}
|
||||
|
||||
// Créer l'abonnement
|
||||
Subscription subscription = Subscription.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.status(SubscriptionStatus.INCOMPLETE) // En attente de la création Stripe
|
||||
.billingCycle(billingCycle)
|
||||
.currentPeriodStart(LocalDateTime.now())
|
||||
.currentPeriodEnd(LocalDateTime.now().plusDays(billingCycle.getMonths() * 30L))
|
||||
.cancelAtPeriodEnd(false)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// Calculer la prochaine date de facturation
|
||||
subscription.setNextBillingDate(subscription.getCurrentPeriodEnd());
|
||||
|
||||
return subscriptionRepository.save(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Subscription updateSubscriptionFromStripe(String stripeSubscriptionId, SubscriptionStatus newStatus) {
|
||||
Subscription subscription = subscriptionRepository.findByStripeSubscriptionId(stripeSubscriptionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Abonnement Stripe non trouvé: " + stripeSubscriptionId));
|
||||
|
||||
SubscriptionStatus oldStatus = subscription.getStatus();
|
||||
subscription.updateStatus(newStatus);
|
||||
|
||||
// Gérer les transitions d'état
|
||||
handleStatusTransition(subscription, oldStatus, newStatus);
|
||||
|
||||
return subscriptionRepository.save(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Subscription changePlan(UUID companyId, Plan newPlan, boolean immediate) {
|
||||
Subscription subscription = getActiveSubscription(companyId);
|
||||
if (subscription == null) {
|
||||
throw new IllegalArgumentException("Aucun abonnement actif trouvé pour cette entreprise");
|
||||
}
|
||||
|
||||
// Mettre à jour l'abonnement
|
||||
subscription.setStripePriceId(newPlan.getStripePriceIdForCycle(subscription.getBillingCycle()));
|
||||
subscription.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
// Si c'est immédiat, mettre à jour la licence aussi
|
||||
if (immediate && subscription.getLicense() != null) {
|
||||
licenseService.upgradeLicense(companyId, newPlan.getType());
|
||||
}
|
||||
|
||||
return subscriptionRepository.save(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Subscription cancelSubscription(UUID companyId, boolean immediate, String reason) {
|
||||
Subscription subscription = getActiveSubscription(companyId);
|
||||
if (subscription == null) {
|
||||
throw new IllegalArgumentException("Aucun abonnement actif trouvé pour cette entreprise");
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
subscription.updateStatus(SubscriptionStatus.CANCELED);
|
||||
// Suspendre la licence immédiatement
|
||||
if (subscription.getLicense() != null) {
|
||||
licenseService.deactivateLicense(subscription.getLicense().getId());
|
||||
}
|
||||
} else {
|
||||
// Programmer l'annulation à la fin de la période
|
||||
subscription.scheduleForCancellation();
|
||||
}
|
||||
|
||||
return subscriptionRepository.save(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Subscription reactivateSubscription(UUID companyId) {
|
||||
// Chercher un abonnement annulé ou suspendu
|
||||
List<Subscription> subscriptions = subscriptionRepository.findByCompanyId(companyId);
|
||||
Subscription subscription = subscriptions.stream()
|
||||
.filter(s -> s.canBeReactivated())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Aucun abonnement réactivable trouvé"));
|
||||
|
||||
subscription.updateStatus(SubscriptionStatus.ACTIVE);
|
||||
subscription.unscheduleForCancellation();
|
||||
|
||||
// Réactiver la licence
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().reactivate();
|
||||
}
|
||||
|
||||
return subscriptionRepository.save(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscription getActiveSubscription(UUID companyId) {
|
||||
return subscriptionRepository.findActiveByCompanyId(companyId)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void handlePaymentFailure(String stripeSubscriptionId, String reason) {
|
||||
Subscription subscription = subscriptionRepository.findByStripeSubscriptionId(stripeSubscriptionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Abonnement non trouvé"));
|
||||
|
||||
// Mettre en statut PAST_DUE
|
||||
subscription.updateStatus(SubscriptionStatus.PAST_DUE);
|
||||
|
||||
// Démarrer la période de grâce pour la licence
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().startGracePeriod(GRACE_PERIOD_DAYS);
|
||||
}
|
||||
|
||||
subscriptionRepository.save(subscription);
|
||||
|
||||
// TODO: Envoyer notification d'échec de paiement
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void handlePaymentSuccess(String stripeSubscriptionId, String stripeInvoiceId) {
|
||||
Subscription subscription = subscriptionRepository.findByStripeSubscriptionId(stripeSubscriptionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Abonnement non trouvé"));
|
||||
|
||||
// Remettre en statut actif si nécessaire
|
||||
if (subscription.getStatus() == SubscriptionStatus.PAST_DUE) {
|
||||
subscription.updateStatus(SubscriptionStatus.ACTIVE);
|
||||
|
||||
// Réactiver la licence
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().reactivate();
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour les dates de période
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
subscription.setCurrentPeriodStart(now);
|
||||
subscription.setCurrentPeriodEnd(now.plusDays(subscription.getBillingCycle().getMonths() * 30L));
|
||||
subscription.setNextBillingDate(subscription.getCurrentPeriodEnd());
|
||||
|
||||
subscriptionRepository.save(subscription);
|
||||
|
||||
// TODO: Envoyer notification de paiement réussi
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> findSubscriptionsRequiringAttention() {
|
||||
List<Subscription> pastDue = subscriptionRepository.findByStatus(SubscriptionStatus.PAST_DUE);
|
||||
List<Subscription> unpaid = subscriptionRepository.findByStatus(SubscriptionStatus.UNPAID);
|
||||
List<Subscription> incomplete = subscriptionRepository.findByStatus(SubscriptionStatus.INCOMPLETE);
|
||||
|
||||
pastDue.addAll(unpaid);
|
||||
pastDue.addAll(incomplete);
|
||||
|
||||
return pastDue;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void startGracePeriod(UUID subscriptionId) {
|
||||
Subscription subscription = subscriptionRepository.findById(subscriptionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Abonnement non trouvé"));
|
||||
|
||||
subscription.updateStatus(SubscriptionStatus.PAST_DUE);
|
||||
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().startGracePeriod(GRACE_PERIOD_DAYS);
|
||||
}
|
||||
|
||||
subscriptionRepository.save(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void suspendUnpaidSubscriptions() {
|
||||
// Récupérer les abonnements en période de grâce expirée
|
||||
List<Subscription> gracePeriodExpired = subscriptionRepository.findInGracePeriod().stream()
|
||||
.filter(subscription -> subscription.getLicense() != null
|
||||
&& subscription.getLicense().getDaysRemainingInGracePeriod() <= 0)
|
||||
.toList();
|
||||
|
||||
for (Subscription subscription : gracePeriodExpired) {
|
||||
subscription.updateStatus(SubscriptionStatus.UNPAID);
|
||||
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().suspend();
|
||||
}
|
||||
|
||||
subscriptionRepository.save(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère les transitions d'état des abonnements
|
||||
*/
|
||||
private void handleStatusTransition(Subscription subscription, SubscriptionStatus oldStatus, SubscriptionStatus newStatus) {
|
||||
// Transition vers ACTIVE
|
||||
if (newStatus == SubscriptionStatus.ACTIVE && oldStatus != SubscriptionStatus.ACTIVE) {
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().reactivate();
|
||||
}
|
||||
}
|
||||
|
||||
// Transition vers CANCELED
|
||||
if (newStatus == SubscriptionStatus.CANCELED) {
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().suspend();
|
||||
}
|
||||
}
|
||||
|
||||
// Transition vers PAST_DUE
|
||||
if (newStatus == SubscriptionStatus.PAST_DUE) {
|
||||
if (subscription.getLicense() != null) {
|
||||
subscription.getLicense().startGracePeriod(GRACE_PERIOD_DAYS);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ===== MÉTHODES AJOUTÉES POUR COMPATIBILITÉ AVEC LA NOUVELLE INTERFACE =====
|
||||
|
||||
@Override
|
||||
public Subscription createSubscription(UUID companyId, String planType, String billingCycle,
|
||||
String paymentMethodId, Boolean startTrial, Integer customTrialDays) {
|
||||
// TODO: Implémenter avec les nouveaux paramètres
|
||||
throw new UnsupportedOperationException("Méthode non encore implémentée");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> findSubscriptions(String status, String billingCycle, String customerId, int page, int size) {
|
||||
// TODO: Implémenter la recherche avec filtres et pagination
|
||||
return subscriptionRepository.findByCompanyId(UUID.fromString(customerId != null ? customerId : "00000000-0000-0000-0000-000000000000"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Subscription> findById(UUID subscriptionId) {
|
||||
return subscriptionRepository.findById(subscriptionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscription changeSubscriptionPlan(UUID subscriptionId, String newPlanType, String newBillingCycle,
|
||||
Boolean enableProration, Boolean immediateChange) {
|
||||
// TODO: Implémenter le changement de plan
|
||||
throw new UnsupportedOperationException("Méthode non encore implémentée");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePaymentMethod(UUID subscriptionId, String newPaymentMethodId) {
|
||||
// TODO: Implémenter la mise à jour du moyen de paiement
|
||||
throw new UnsupportedOperationException("Méthode non encore implémentée");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscription scheduleForCancellation(UUID subscriptionId, String cancellationReason) {
|
||||
// TODO: Implémenter la planification d'annulation
|
||||
throw new UnsupportedOperationException("Méthode non encore implémentée");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscription cancelSubscriptionImmediately(UUID subscriptionId, String reason) {
|
||||
// TODO: Implémenter l'annulation immédiate
|
||||
throw new UnsupportedOperationException("Méthode non encore implémentée");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> findByCompanyId(UUID companyId) {
|
||||
return subscriptionRepository.findByCompanyId(companyId);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<Subscription> findTrialsEndingSoon(int daysAhead) {
|
||||
return subscriptionRepository.findTrialsEndingSoon(daysAhead);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,108 +1,116 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
||||
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserServiceImpl(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassword(ChangePasswordRequest request, Principal connectedUser) {
|
||||
userRepository.changePassword(request, connectedUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAccount createUser(RegisterRequest request) {
|
||||
// Create UserAccount from RegisterRequest
|
||||
UserAccount userAccount = UserAccount.builder()
|
||||
.firstName(request.getFirstName())
|
||||
.lastName(request.getLastName())
|
||||
.email(new com.dh7789dev.xpeditis.dto.valueobject.Email(request.getEmail()))
|
||||
.username(request.getUsername())
|
||||
.phoneNumber(new com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber(request.getPhoneNumber()))
|
||||
.authProvider(request.getAuthProvider())
|
||||
.privacyPolicyAccepted(request.isPrivacyPolicyAccepted())
|
||||
.isActive(true)
|
||||
.build();
|
||||
|
||||
return userRepository.save(userAccount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAccount createGoogleUser(String googleToken) {
|
||||
// TODO: Implement Google OAuth integration to extract user info from token
|
||||
throw new UnsupportedOperationException("Google OAuth integration not implemented yet");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UserAccount> findById(UUID id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UserAccount> findByEmail(String email) {
|
||||
return userRepository.findByEmail(email);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UserAccount> findByUsername(String username) {
|
||||
return userRepository.findByUsername(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAccount updateProfile(UserAccount userAccount) {
|
||||
return userRepository.save(userAccount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivateUser(UUID userId) {
|
||||
userRepository.deactivateUser(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUser(UUID userId) {
|
||||
userRepository.deleteById(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByEmail(String email) {
|
||||
return userRepository.existsByEmail(email);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByUsername(String username) {
|
||||
return userRepository.existsByUsername(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<UserAccount> findAllUsers(int page, int size) {
|
||||
return userRepository.findAllUsers(page, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<UserAccount> findUsersByCompany(UUID companyId, int page, int size) {
|
||||
return userRepository.findUsersByCompany(companyId, page, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countAllUsers() {
|
||||
return userRepository.countAllUsers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countUsersByCompany(UUID companyId) {
|
||||
return userRepository.countUsersByCompany(companyId);
|
||||
}
|
||||
}
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.security.Principal;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserServiceImpl(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassword(ChangePasswordRequest request, Principal connectedUser) {
|
||||
userRepository.changePassword(request, connectedUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAccount createUser(RegisterRequest request) {
|
||||
// Create UserAccount from RegisterRequest
|
||||
UserAccount userAccount = UserAccount.builder()
|
||||
.firstName(request.getFirstName())
|
||||
.lastName(request.getLastName())
|
||||
.email(new com.dh7789dev.xpeditis.dto.valueobject.Email(request.getEmail()))
|
||||
.username(request.getUsername())
|
||||
.phoneNumber(new com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber(request.getPhoneNumber()))
|
||||
.authProvider(request.getAuthProvider())
|
||||
.privacyPolicyAccepted(request.isPrivacyPolicyAccepted())
|
||||
.isActive(true)
|
||||
.build();
|
||||
|
||||
return userRepository.save(userAccount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAccount createGoogleUser(String googleToken) {
|
||||
// TODO: Implement Google OAuth integration to extract user info from token
|
||||
throw new UnsupportedOperationException("Google OAuth integration not implemented yet");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UserAccount> findById(UUID id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UserAccount> findByEmail(String email) {
|
||||
return userRepository.findByEmail(email);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UserAccount> findByUsername(String username) {
|
||||
return userRepository.findByUsername(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAccount updateProfile(UserAccount userAccount) {
|
||||
return userRepository.save(userAccount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivateUser(UUID userId) {
|
||||
userRepository.deactivateUser(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUser(UUID userId) {
|
||||
userRepository.deleteById(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByEmail(String email) {
|
||||
return userRepository.existsByEmail(email);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByUsername(String username) {
|
||||
return userRepository.existsByUsername(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<UserAccount> findAllUsers(int page, int size) {
|
||||
return userRepository.findAllUsers(page, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<UserAccount> findUsersByCompany(UUID companyId, int page, int size) {
|
||||
return userRepository.findUsersByCompany(companyId, page, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countAllUsers() {
|
||||
return userRepository.countAllUsers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countUsersByCompany(UUID companyId) {
|
||||
return userRepository.countUsersByCompany(companyId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class VesselScheduleServiceImpl implements VesselScheduleService {
|
||||
}
|
||||
|
||||
@ -1,277 +0,0 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.*;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("DevisCalculService - Tests unitaires")
|
||||
class DevisCalculServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private GrilleTarifaireService grilleTarifaireService;
|
||||
|
||||
private DevisCalculServiceImpl devisCalculService;
|
||||
private DemandeDevis demandeDevisValide;
|
||||
private GrilleTarifaire grilleTarifaireStandard;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
devisCalculService = new DevisCalculServiceImpl(grilleTarifaireService);
|
||||
|
||||
// Créer une demande de devis valide pour les tests
|
||||
demandeDevisValide = creerDemandeDevisValide();
|
||||
|
||||
// Créer une grille tarifaire standard pour les tests
|
||||
grilleTarifaireStandard = creerGrilleTarifaireStandard();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit calculer 3 offres avec succès")
|
||||
void doitCalculerTroisOffresAvecSucces() {
|
||||
// Given
|
||||
when(grilleTarifaireService.trouverGrillesApplicables(any(DemandeDevis.class)))
|
||||
.thenReturn(Arrays.asList(grilleTarifaireStandard));
|
||||
|
||||
// When
|
||||
ReponseDevis reponse = devisCalculService.calculerTroisOffres(demandeDevisValide);
|
||||
|
||||
// Then
|
||||
assertThat(reponse).isNotNull();
|
||||
assertThat(reponse.getOffres()).hasSize(3);
|
||||
|
||||
// Vérifier que les 3 types d'offres sont présents
|
||||
List<String> typesOffres = reponse.getOffres().stream()
|
||||
.map(OffreCalculee::getType)
|
||||
.toList();
|
||||
|
||||
assertThat(typesOffres).containsExactlyInAnyOrder("RAPIDE", "STANDARD", "ECONOMIQUE");
|
||||
|
||||
// Vérifier que l'offre rapide est la plus chère
|
||||
OffreCalculee offreRapide = reponse.getOffres().stream()
|
||||
.filter(o -> "RAPIDE".equals(o.getType()))
|
||||
.findFirst().orElseThrow();
|
||||
|
||||
OffreCalculee offreStandard = reponse.getOffres().stream()
|
||||
.filter(o -> "STANDARD".equals(o.getType()))
|
||||
.findFirst().orElseThrow();
|
||||
|
||||
OffreCalculee offreEconomique = reponse.getOffres().stream()
|
||||
.filter(o -> "ECONOMIQUE".equals(o.getType()))
|
||||
.findFirst().orElseThrow();
|
||||
|
||||
assertThat(offreRapide.getPrixTotal()).isGreaterThan(offreStandard.getPrixTotal());
|
||||
assertThat(offreStandard.getPrixTotal()).isGreaterThan(offreEconomique.getPrixTotal());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit calculer correctement le colisage résumé")
|
||||
void doitCalculerCorrectementColisageResume() {
|
||||
// Given
|
||||
when(grilleTarifaireService.trouverGrillesApplicables(any(DemandeDevis.class)))
|
||||
.thenReturn(Arrays.asList(grilleTarifaireStandard));
|
||||
|
||||
DemandeDevis demande = creerDemandeAvecColisage();
|
||||
|
||||
// When
|
||||
ReponseDevis reponse = devisCalculService.calculerTroisOffres(demande);
|
||||
|
||||
// Then
|
||||
ReponseDevis.ColisageResume colisage = reponse.getColisageResume();
|
||||
assertThat(colisage.getNombreColis()).isEqualTo(3); // 2 + 1
|
||||
assertThat(colisage.getPoidsTotal()).isEqualTo(350.0); // (100*2) + (150*1)
|
||||
assertThat(colisage.getVolumeTotal()).isEqualTo(0.35); // (0.1*2) + (0.15*1)
|
||||
|
||||
// Le poids taxable doit être le max entre poids réel et poids volumétrique
|
||||
double poidsVolumetrique = 0.35 * 250; // 87.5 kg
|
||||
assertThat(colisage.getPoidsTaxable()).isEqualTo(350.0); // Poids réel > poids volumétrique
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit lever une exception si aucune grille applicable")
|
||||
void doitLeverExceptionSiAucuneGrilleApplicable() {
|
||||
// Given
|
||||
when(grilleTarifaireService.trouverGrillesApplicables(any(DemandeDevis.class)))
|
||||
.thenReturn(Arrays.asList());
|
||||
|
||||
// When & Then
|
||||
assertThatThrownBy(() -> devisCalculService.calculerTroisOffres(demandeDevisValide))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("Aucune grille tarifaire applicable");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit valider correctement une demande de devis valide")
|
||||
void doitValiderCorrectementDemandeValide() {
|
||||
// When & Then
|
||||
assertThatCode(() -> devisCalculService.validerDemandeDevis(demandeDevisValide))
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit lever une exception si demande de devis nulle")
|
||||
void doitLeverExceptionSiDemandeNulle() {
|
||||
// When & Then
|
||||
assertThatThrownBy(() -> devisCalculService.validerDemandeDevis(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("ne peut pas être nulle");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit lever une exception si adresses manquantes")
|
||||
void doitLeverExceptionSiAdressesManquantes() {
|
||||
// Given
|
||||
DemandeDevis demande = creerDemandeDevisValide();
|
||||
demande.setDepart(null);
|
||||
|
||||
// When & Then
|
||||
assertThatThrownBy(() -> devisCalculService.validerDemandeDevis(demande))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("adresses de départ et d'arrivée sont obligatoires");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit lever une exception si aucun colisage")
|
||||
void doitLeverExceptionSiAucunColisage() {
|
||||
// Given
|
||||
DemandeDevis demande = creerDemandeDevisValide();
|
||||
demande.setColisages(Arrays.asList());
|
||||
|
||||
// When & Then
|
||||
assertThatThrownBy(() -> devisCalculService.validerDemandeDevis(demande))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Au moins un colisage doit être défini");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit lever une exception si poids invalide")
|
||||
void doitLeverExceptionSiPoidsInvalide() {
|
||||
// Given
|
||||
DemandeDevis demande = creerDemandeDevisValide();
|
||||
demande.getColisages().get(0).setPoids(0.0);
|
||||
|
||||
// When & Then
|
||||
assertThatThrownBy(() -> devisCalculService.validerDemandeDevis(demande))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Le poids de chaque colisage doit être supérieur à 0");
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Méthodes utilitaires pour créer les objets de test
|
||||
// ================================
|
||||
|
||||
private DemandeDevis creerDemandeDevisValide() {
|
||||
DemandeDevis demande = new DemandeDevis();
|
||||
demande.setTypeService("EXPORT");
|
||||
demande.setIncoterm("FOB");
|
||||
demande.setTypeLivraison("Door to Door");
|
||||
demande.setNomClient("Test Client");
|
||||
demande.setEmailClient("test@example.com");
|
||||
|
||||
// Adresses
|
||||
DemandeDevis.AdresseTransport depart = new DemandeDevis.AdresseTransport();
|
||||
depart.setVille("Lyon");
|
||||
depart.setCodePostal("69000");
|
||||
depart.setPays("FRA");
|
||||
demande.setDepart(depart);
|
||||
|
||||
DemandeDevis.AdresseTransport arrivee = new DemandeDevis.AdresseTransport();
|
||||
arrivee.setVille("Shanghai");
|
||||
arrivee.setCodePostal("200000");
|
||||
arrivee.setPays("CHN");
|
||||
demande.setArrivee(arrivee);
|
||||
|
||||
// Colisage simple
|
||||
DemandeDevis.Colisage colisage = new DemandeDevis.Colisage();
|
||||
colisage.setType(DemandeDevis.Colisage.TypeColisage.COLIS);
|
||||
colisage.setQuantite(1);
|
||||
colisage.setLongueur(50.0);
|
||||
colisage.setLargeur(40.0);
|
||||
colisage.setHauteur(30.0);
|
||||
colisage.setPoids(25.0);
|
||||
|
||||
demande.setColisages(Arrays.asList(colisage));
|
||||
demande.setDateEnlevement(LocalDate.now().plusDays(7));
|
||||
|
||||
return demande;
|
||||
}
|
||||
|
||||
private DemandeDevis creerDemandeAvecColisage() {
|
||||
DemandeDevis demande = creerDemandeDevisValide();
|
||||
|
||||
// Premier colisage
|
||||
DemandeDevis.Colisage colisage1 = new DemandeDevis.Colisage();
|
||||
colisage1.setType(DemandeDevis.Colisage.TypeColisage.COLIS);
|
||||
colisage1.setQuantite(2);
|
||||
colisage1.setLongueur(50.0);
|
||||
colisage1.setLargeur(40.0);
|
||||
colisage1.setHauteur(50.0); // Volume = 0.1 m³
|
||||
colisage1.setPoids(100.0);
|
||||
|
||||
// Deuxième colisage
|
||||
DemandeDevis.Colisage colisage2 = new DemandeDevis.Colisage();
|
||||
colisage2.setType(DemandeDevis.Colisage.TypeColisage.PALETTE);
|
||||
colisage2.setQuantite(1);
|
||||
colisage2.setLongueur(120.0);
|
||||
colisage2.setLargeur(80.0);
|
||||
colisage2.setHauteur(150.0); // Volume = 0.15 m³
|
||||
colisage2.setPoids(150.0);
|
||||
colisage2.setGerbable(true);
|
||||
|
||||
demande.setColisages(Arrays.asList(colisage1, colisage2));
|
||||
|
||||
return demande;
|
||||
}
|
||||
|
||||
private GrilleTarifaire creerGrilleTarifaireStandard() {
|
||||
GrilleTarifaire grille = new GrilleTarifaire();
|
||||
grille.setId(1L);
|
||||
grille.setNomGrille("Test Grille Standard");
|
||||
grille.setTransporteur("LESCHACO");
|
||||
grille.setTypeService(GrilleTarifaire.TypeService.EXPORT);
|
||||
grille.setOriginePays("FRA");
|
||||
grille.setDestinationPays("CHN");
|
||||
grille.setModeTransport(GrilleTarifaire.ModeTransport.MARITIME);
|
||||
grille.setServiceType(GrilleTarifaire.ServiceType.STANDARD);
|
||||
grille.setTransitTimeMin(25);
|
||||
grille.setTransitTimeMax(30);
|
||||
grille.setValiditeDebut(LocalDate.now().minusDays(30));
|
||||
grille.setValiditeFin(LocalDate.now().plusDays(60));
|
||||
grille.setDevise("EUR");
|
||||
|
||||
// Tarif de fret
|
||||
TarifFret tarif = new TarifFret();
|
||||
tarif.setPoidsMin(BigDecimal.ZERO);
|
||||
tarif.setPoidsMax(BigDecimal.valueOf(1000));
|
||||
tarif.setTauxUnitaire(BigDecimal.valueOf(2.5));
|
||||
tarif.setUniteFacturation(TarifFret.UniteFacturation.KG);
|
||||
tarif.setMinimumFacturation(BigDecimal.valueOf(100));
|
||||
|
||||
grille.setTarifsFret(Arrays.asList(tarif));
|
||||
|
||||
// Frais additionnels obligatoires
|
||||
FraisAdditionnels fraisDoc = new FraisAdditionnels();
|
||||
fraisDoc.setTypeFrais("DOCUMENTATION");
|
||||
fraisDoc.setMontant(BigDecimal.valueOf(32));
|
||||
fraisDoc.setUniteFacturation(FraisAdditionnels.UniteFacturation.LS);
|
||||
fraisDoc.setObligatoire(true);
|
||||
|
||||
grille.setFraisAdditionnels(Arrays.asList(fraisDoc));
|
||||
grille.setSurchargesDangereuses(Arrays.asList());
|
||||
|
||||
return grille;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package com.dh7789dev.xpeditis.dao;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.Invoice;
|
||||
import com.dh7789dev.xpeditis.entity.InvoiceEntity;
|
||||
import com.dh7789dev.xpeditis.mapper.InvoiceMapper;
|
||||
import com.dh7789dev.xpeditis.port.out.InvoiceRepository;
|
||||
import com.dh7789dev.xpeditis.repository.InvoiceJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Implémentation DAO du InvoiceRepository
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class InvoiceDao implements InvoiceRepository {
|
||||
|
||||
private final InvoiceJpaRepository invoiceJpaRepository;
|
||||
private final InvoiceMapper invoiceMapper;
|
||||
|
||||
@Override
|
||||
public Optional<Invoice> findById(UUID id) {
|
||||
return invoiceJpaRepository.findById(id)
|
||||
.map(invoiceMapper::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Invoice> findByStripeInvoiceId(String stripeInvoiceId) {
|
||||
return invoiceJpaRepository.findByStripeInvoiceId(stripeInvoiceId)
|
||||
.map(invoiceMapper::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Invoice> findBySubscriptionId(UUID subscriptionId) {
|
||||
return invoiceMapper.toDomainList(
|
||||
invoiceJpaRepository.findBySubscriptionIdOrderByCreatedAtDesc(subscriptionId)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Invoice> findUnpaidInvoices() {
|
||||
return invoiceMapper.toDomainList(
|
||||
invoiceJpaRepository.findUnpaidInvoices()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Invoice save(Invoice invoice) {
|
||||
InvoiceEntity entity;
|
||||
|
||||
if (invoice.getId() != null) {
|
||||
entity = invoiceJpaRepository.findById(invoice.getId())
|
||||
.orElseGet(() -> invoiceMapper.toEntity(invoice));
|
||||
invoiceMapper.updateEntity(entity, invoice);
|
||||
} else {
|
||||
entity = invoiceMapper.toEntity(invoice);
|
||||
}
|
||||
|
||||
InvoiceEntity saved = invoiceJpaRepository.save(entity);
|
||||
return invoiceMapper.toDomain(saved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(UUID id) {
|
||||
invoiceJpaRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
package com.dh7789dev.xpeditis.dao;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||
import com.dh7789dev.xpeditis.dto.app.Plan;
|
||||
import com.dh7789dev.xpeditis.entity.PlanEntity;
|
||||
import com.dh7789dev.xpeditis.mapper.PlanMapper;
|
||||
import com.dh7789dev.xpeditis.port.out.PlanRepository;
|
||||
import com.dh7789dev.xpeditis.repository.PlanJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implémentation DAO du PlanRepository
|
||||
* Fait le pont entre le domaine et la couche de persistance JPA
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PlanDao implements PlanRepository {
|
||||
|
||||
private final PlanJpaRepository planJpaRepository;
|
||||
private final PlanMapper planMapper;
|
||||
|
||||
@Override
|
||||
public Optional<Plan> findById(UUID id) {
|
||||
return planJpaRepository.findById(id)
|
||||
.map(planMapper::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plan> findAllActive() {
|
||||
return planJpaRepository.findByIsActiveTrueOrderByDisplayOrderAsc()
|
||||
.stream()
|
||||
.map(planMapper::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Plan> findByName(String name) {
|
||||
return planJpaRepository.findByName(name)
|
||||
.map(planMapper::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plan save(Plan plan) {
|
||||
PlanEntity entity;
|
||||
|
||||
if (plan.getId() != null) {
|
||||
// Update existing
|
||||
entity = planJpaRepository.findById(plan.getId())
|
||||
.orElseGet(() -> planMapper.toEntity(plan));
|
||||
planMapper.updateEntity(entity, plan);
|
||||
} else {
|
||||
// Create new
|
||||
entity = planMapper.toEntity(plan);
|
||||
}
|
||||
|
||||
PlanEntity saved = planJpaRepository.save(entity);
|
||||
return planMapper.toDomain(saved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(UUID id) {
|
||||
planJpaRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plan> findAllByIsActiveTrue() {
|
||||
return planJpaRepository.findByIsActiveTrueOrderByDisplayOrderAsc()
|
||||
.stream()
|
||||
.map(planMapper::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plan> findByTypeAndIsActiveTrue(LicenseType type) {
|
||||
return planJpaRepository.findByType(type.name())
|
||||
.map(entity -> List.of(planMapper.toDomain(entity)))
|
||||
.orElse(List.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Plan> findByStripePriceId(String stripePriceId) {
|
||||
return planJpaRepository.findByAnyStripePriceId(stripePriceId)
|
||||
.map(planMapper::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plan> findSuitableForUserCount(int userCount) {
|
||||
return planJpaRepository.findCheapestPlansForUsers(userCount)
|
||||
.stream()
|
||||
.map(planMapper::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
package com.dh7789dev.xpeditis.dao;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.Subscription;
|
||||
import com.dh7789dev.xpeditis.dto.app.SubscriptionStatus;
|
||||
import com.dh7789dev.xpeditis.entity.SubscriptionEntity;
|
||||
import com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity;
|
||||
import com.dh7789dev.xpeditis.mapper.SubscriptionMapper;
|
||||
import com.dh7789dev.xpeditis.port.out.SubscriptionRepository;
|
||||
import com.dh7789dev.xpeditis.repository.SubscriptionJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implémentation DAO du SubscriptionRepository
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SubscriptionDao implements SubscriptionRepository {
|
||||
|
||||
private final SubscriptionJpaRepository subscriptionJpaRepository;
|
||||
private final SubscriptionMapper subscriptionMapper;
|
||||
|
||||
@Override
|
||||
public Optional<Subscription> findById(UUID id) {
|
||||
return subscriptionJpaRepository.findById(id)
|
||||
.map(subscriptionMapper::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Subscription> findByStripeSubscriptionId(String stripeSubscriptionId) {
|
||||
return subscriptionJpaRepository.findByStripeSubscriptionId(stripeSubscriptionId)
|
||||
.map(subscriptionMapper::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> findByCompanyId(UUID companyId) {
|
||||
// Note: SubscriptionEntity n'a pas de companyId, on utilise stripeCustomerId
|
||||
// Cette méthode retournera une liste vide pour l'instant
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> findSubscriptionsRequiringAttention() {
|
||||
List<SubscriptionEntity> pastDue = subscriptionJpaRepository.findSubscriptionsInGracePeriod();
|
||||
return subscriptionMapper.toDomainList(pastDue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> findTrialsEndingSoon(int daysAhead) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime futureDate = now.plusDays(daysAhead);
|
||||
return subscriptionMapper.toDomainList(
|
||||
subscriptionJpaRepository.findTrialsEndingSoon(now, futureDate)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> findSubscriptions(String status, String billingCycle, String customerId, int page, int size) {
|
||||
// Implémentation simplifiée
|
||||
if (status != null) {
|
||||
SubscriptionStatusEntity statusEntity = SubscriptionStatusEntity.valueOf(status);
|
||||
return subscriptionMapper.toDomainList(
|
||||
subscriptionJpaRepository.findByStatusOrderByCreatedAtDesc(statusEntity)
|
||||
);
|
||||
}
|
||||
return subscriptionMapper.toDomainList(
|
||||
subscriptionJpaRepository.findAll()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscription save(Subscription subscription) {
|
||||
SubscriptionEntity entity;
|
||||
|
||||
if (subscription.getId() != null) {
|
||||
entity = subscriptionJpaRepository.findById(subscription.getId())
|
||||
.orElseGet(() -> subscriptionMapper.toEntity(subscription));
|
||||
subscriptionMapper.updateEntity(entity, subscription);
|
||||
} else {
|
||||
entity = subscriptionMapper.toEntity(subscription);
|
||||
}
|
||||
|
||||
SubscriptionEntity saved = subscriptionJpaRepository.save(entity);
|
||||
return subscriptionMapper.toDomain(saved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(UUID id) {
|
||||
subscriptionJpaRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> findByStatus(SubscriptionStatus status) {
|
||||
SubscriptionStatusEntity statusEntity = SubscriptionStatusEntity.valueOf(status.name());
|
||||
return subscriptionMapper.toDomainList(
|
||||
subscriptionJpaRepository.findByStatusOrderByCreatedAtDesc(statusEntity)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> findInGracePeriod() {
|
||||
return subscriptionMapper.toDomainList(
|
||||
subscriptionJpaRepository.findSubscriptionsInGracePeriod()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Subscription> findActiveByCompanyId(UUID companyId) {
|
||||
// Note: SubscriptionEntity n'a pas de companyId
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@ -1,245 +1,233 @@
|
||||
package com.dh7789dev.xpeditis.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entité JPA pour les factures Stripe
|
||||
*/
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@FieldNameConstants
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@Table(name = "invoices")
|
||||
@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class)
|
||||
public class InvoiceEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(columnDefinition = "BINARY(16)")
|
||||
UUID id;
|
||||
|
||||
@Column(name = "stripe_invoice_id", unique = true, nullable = false)
|
||||
String stripeInvoiceId;
|
||||
|
||||
@Column(name = "invoice_number", unique = true, nullable = false, length = 50)
|
||||
String invoiceNumber;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "subscription_id", nullable = false, foreignKey = @ForeignKey(name = "fk_invoice_subscription"))
|
||||
SubscriptionEntity subscription;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 50)
|
||||
InvoiceStatusEntity status;
|
||||
|
||||
@Column(name = "amount_due", nullable = false, precision = 10, scale = 2)
|
||||
BigDecimal amountDue;
|
||||
|
||||
@Column(name = "amount_paid", precision = 10, scale = 2)
|
||||
BigDecimal amountPaid = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "currency", nullable = false, length = 3)
|
||||
String currency = "EUR";
|
||||
|
||||
@Column(name = "billing_period_start", nullable = false)
|
||||
LocalDateTime billingPeriodStart;
|
||||
|
||||
@Column(name = "billing_period_end", nullable = false)
|
||||
LocalDateTime billingPeriodEnd;
|
||||
|
||||
@Column(name = "due_date")
|
||||
LocalDateTime dueDate;
|
||||
|
||||
@Column(name = "paid_at")
|
||||
LocalDateTime paidAt;
|
||||
|
||||
@Column(name = "invoice_pdf_url", columnDefinition = "TEXT")
|
||||
String invoicePdfUrl;
|
||||
|
||||
@Column(name = "hosted_invoice_url", columnDefinition = "TEXT")
|
||||
String hostedInvoiceUrl;
|
||||
|
||||
@Column(name = "attempt_count", nullable = false)
|
||||
Integer attemptCount = 0;
|
||||
|
||||
@OneToMany(mappedBy = "invoice", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
List<InvoiceLineItemEntity> lineItems;
|
||||
|
||||
@Column(name = "created_at", updatable = false)
|
||||
LocalDateTime createdAt;
|
||||
|
||||
@org.springframework.data.annotation.CreatedDate
|
||||
@Column(name = "created_date", updatable = false, nullable = false)
|
||||
java.time.Instant createdDate;
|
||||
|
||||
@org.springframework.data.annotation.LastModifiedDate
|
||||
@Column(name = "modified_date", nullable = false)
|
||||
java.time.Instant modifiedDate;
|
||||
|
||||
@org.springframework.data.annotation.CreatedBy
|
||||
@Column(name = "created_by", updatable = false, nullable = false)
|
||||
String createdBy = "SYSTEM";
|
||||
|
||||
@org.springframework.data.annotation.LastModifiedBy
|
||||
@Column(name = "modified_by")
|
||||
String modifiedBy;
|
||||
|
||||
@PrePersist
|
||||
public void onCreate() {
|
||||
if (id == null) {
|
||||
id = UUID.randomUUID();
|
||||
}
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est payée
|
||||
*/
|
||||
public boolean isPaid() {
|
||||
return status == InvoiceStatusEntity.PAID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est en attente de paiement
|
||||
*/
|
||||
public boolean isPending() {
|
||||
return status == InvoiceStatusEntity.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est en retard
|
||||
*/
|
||||
public boolean isOverdue() {
|
||||
return dueDate != null && LocalDateTime.now().isAfter(dueDate) && !isPaid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours de retard (0 si pas en retard)
|
||||
*/
|
||||
public long getDaysOverdue() {
|
||||
if (!isOverdue()) return 0;
|
||||
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(dueDate, LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le montant restant à payer
|
||||
*/
|
||||
public BigDecimal getRemainingAmount() {
|
||||
if (amountPaid == null) return amountDue;
|
||||
return amountDue.subtract(amountPaid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est partiellement payée
|
||||
*/
|
||||
public boolean isPartiallyPaid() {
|
||||
return amountPaid != null &&
|
||||
amountPaid.compareTo(BigDecimal.ZERO) > 0 &&
|
||||
amountPaid.compareTo(amountDue) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le pourcentage de paiement effectué
|
||||
*/
|
||||
public double getPaymentPercentage() {
|
||||
if (amountDue.compareTo(BigDecimal.ZERO) == 0) return 100.0;
|
||||
if (amountPaid == null) return 0.0;
|
||||
|
||||
return amountPaid.divide(amountDue, 4, java.math.RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100))
|
||||
.doubleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return la durée de la période de facturation en jours
|
||||
*/
|
||||
public long getBillingPeriodDays() {
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(billingPeriodStart, billingPeriodEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette facture nécessite une attention (retard, échecs répétés)
|
||||
*/
|
||||
public boolean requiresAttention() {
|
||||
return isOverdue() ||
|
||||
(attemptCount != null && attemptCount > 3) ||
|
||||
status == InvoiceStatusEntity.PAYMENT_FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le montant total des lignes de facture
|
||||
*/
|
||||
public BigDecimal calculateTotalFromLineItems() {
|
||||
if (lineItems == null || lineItems.isEmpty()) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
return lineItems.stream()
|
||||
.map(InvoiceLineItemEntity::getAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture inclut du prorata
|
||||
*/
|
||||
public boolean hasProrationItems() {
|
||||
return lineItems != null &&
|
||||
lineItems.stream().anyMatch(InvoiceLineItemEntity::getProrated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment le nombre de tentatives de paiement
|
||||
*/
|
||||
public void incrementAttemptCount() {
|
||||
if (attemptCount == null) {
|
||||
attemptCount = 1;
|
||||
} else {
|
||||
attemptCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque la facture comme payée
|
||||
*/
|
||||
public void markAsPaid(LocalDateTime paidDate) {
|
||||
this.status = InvoiceStatusEntity.PAID;
|
||||
this.paidAt = paidDate;
|
||||
this.amountPaid = this.amountDue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque la facture comme échouée
|
||||
*/
|
||||
public void markAsFailed() {
|
||||
this.status = InvoiceStatusEntity.PAYMENT_FAILED;
|
||||
incrementAttemptCount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Énumération des statuts de facture
|
||||
*/
|
||||
enum InvoiceStatusEntity {
|
||||
DRAFT, // Brouillon
|
||||
OPEN, // En attente de paiement
|
||||
PAID, // Payée
|
||||
PAYMENT_FAILED, // Échec de paiement
|
||||
VOIDED, // Annulée
|
||||
UNCOLLECTIBLE // Irrécouvrable
|
||||
}
|
||||
package com.dh7789dev.xpeditis.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entité JPA pour les factures Stripe
|
||||
*/
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@FieldNameConstants
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@Table(name = "invoices")
|
||||
@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class)
|
||||
public class InvoiceEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(columnDefinition = "BINARY(16)")
|
||||
UUID id;
|
||||
|
||||
@Column(name = "stripe_invoice_id", unique = true, nullable = false)
|
||||
String stripeInvoiceId;
|
||||
|
||||
@Column(name = "invoice_number", unique = true, nullable = false, length = 50)
|
||||
String invoiceNumber;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "subscription_id", nullable = false, foreignKey = @ForeignKey(name = "fk_invoice_subscription"))
|
||||
SubscriptionEntity subscription;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 50)
|
||||
InvoiceStatusEntity status;
|
||||
|
||||
@Column(name = "amount_due", nullable = false, precision = 10, scale = 2)
|
||||
BigDecimal amountDue;
|
||||
|
||||
@Column(name = "amount_paid", precision = 10, scale = 2)
|
||||
BigDecimal amountPaid = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "currency", nullable = false, length = 3)
|
||||
String currency = "EUR";
|
||||
|
||||
@Column(name = "billing_period_start", nullable = false)
|
||||
LocalDateTime billingPeriodStart;
|
||||
|
||||
@Column(name = "billing_period_end", nullable = false)
|
||||
LocalDateTime billingPeriodEnd;
|
||||
|
||||
@Column(name = "due_date")
|
||||
LocalDateTime dueDate;
|
||||
|
||||
@Column(name = "paid_at")
|
||||
LocalDateTime paidAt;
|
||||
|
||||
@Column(name = "invoice_pdf_url", columnDefinition = "TEXT")
|
||||
String invoicePdfUrl;
|
||||
|
||||
@Column(name = "hosted_invoice_url", columnDefinition = "TEXT")
|
||||
String hostedInvoiceUrl;
|
||||
|
||||
@Column(name = "attempt_count", nullable = false)
|
||||
Integer attemptCount = 0;
|
||||
|
||||
@OneToMany(mappedBy = "invoice", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
List<InvoiceLineItemEntity> lineItems;
|
||||
|
||||
@Column(name = "created_at", updatable = false)
|
||||
LocalDateTime createdAt;
|
||||
|
||||
@org.springframework.data.annotation.CreatedDate
|
||||
@Column(name = "created_date", updatable = false, nullable = false)
|
||||
java.time.Instant createdDate;
|
||||
|
||||
@org.springframework.data.annotation.LastModifiedDate
|
||||
@Column(name = "modified_date", nullable = false)
|
||||
java.time.Instant modifiedDate;
|
||||
|
||||
@org.springframework.data.annotation.CreatedBy
|
||||
@Column(name = "created_by", updatable = false, nullable = false)
|
||||
String createdBy = "SYSTEM";
|
||||
|
||||
@org.springframework.data.annotation.LastModifiedBy
|
||||
@Column(name = "modified_by")
|
||||
String modifiedBy;
|
||||
|
||||
@PrePersist
|
||||
public void onCreate() {
|
||||
if (id == null) {
|
||||
id = UUID.randomUUID();
|
||||
}
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est payée
|
||||
*/
|
||||
public boolean isPaid() {
|
||||
return status == InvoiceStatusEntity.PAID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est en attente de paiement
|
||||
*/
|
||||
public boolean isPending() {
|
||||
return status == InvoiceStatusEntity.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est en retard
|
||||
*/
|
||||
public boolean isOverdue() {
|
||||
return dueDate != null && LocalDateTime.now().isAfter(dueDate) && !isPaid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours de retard (0 si pas en retard)
|
||||
*/
|
||||
public long getDaysOverdue() {
|
||||
if (!isOverdue()) return 0;
|
||||
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(dueDate, LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le montant restant à payer
|
||||
*/
|
||||
public BigDecimal getRemainingAmount() {
|
||||
if (amountPaid == null) return amountDue;
|
||||
return amountDue.subtract(amountPaid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est partiellement payée
|
||||
*/
|
||||
public boolean isPartiallyPaid() {
|
||||
return amountPaid != null &&
|
||||
amountPaid.compareTo(BigDecimal.ZERO) > 0 &&
|
||||
amountPaid.compareTo(amountDue) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le pourcentage de paiement effectué
|
||||
*/
|
||||
public double getPaymentPercentage() {
|
||||
if (amountDue.compareTo(BigDecimal.ZERO) == 0) return 100.0;
|
||||
if (amountPaid == null) return 0.0;
|
||||
|
||||
return amountPaid.divide(amountDue, 4, java.math.RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100))
|
||||
.doubleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return la durée de la période de facturation en jours
|
||||
*/
|
||||
public long getBillingPeriodDays() {
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(billingPeriodStart, billingPeriodEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette facture nécessite une attention (retard, échecs répétés)
|
||||
*/
|
||||
public boolean requiresAttention() {
|
||||
return isOverdue() ||
|
||||
(attemptCount != null && attemptCount > 3) ||
|
||||
status == InvoiceStatusEntity.PAYMENT_FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le montant total des lignes de facture
|
||||
*/
|
||||
public BigDecimal calculateTotalFromLineItems() {
|
||||
if (lineItems == null || lineItems.isEmpty()) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
return lineItems.stream()
|
||||
.map(InvoiceLineItemEntity::getAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture inclut du prorata
|
||||
*/
|
||||
public boolean hasProrationItems() {
|
||||
return lineItems != null &&
|
||||
lineItems.stream().anyMatch(InvoiceLineItemEntity::getProrated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment le nombre de tentatives de paiement
|
||||
*/
|
||||
public void incrementAttemptCount() {
|
||||
if (attemptCount == null) {
|
||||
attemptCount = 1;
|
||||
} else {
|
||||
attemptCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque la facture comme payée
|
||||
*/
|
||||
public void markAsPaid(LocalDateTime paidDate) {
|
||||
this.status = InvoiceStatusEntity.PAID;
|
||||
this.paidAt = paidDate;
|
||||
this.amountPaid = this.amountDue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque la facture comme échouée
|
||||
*/
|
||||
public void markAsFailed() {
|
||||
this.status = InvoiceStatusEntity.PAYMENT_FAILED;
|
||||
incrementAttemptCount();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package com.dh7789dev.xpeditis.entity;
|
||||
|
||||
/**
|
||||
* Énumération des statuts de facture
|
||||
*/
|
||||
public enum InvoiceStatusEntity {
|
||||
DRAFT, // Brouillon
|
||||
OPEN, // En attente de paiement
|
||||
PAID, // Payée
|
||||
PAYMENT_FAILED, // Échec de paiement
|
||||
VOIDED, // Annulée
|
||||
UNCOLLECTIBLE // Irrécouvrable
|
||||
}
|
||||
@ -1,260 +1,245 @@
|
||||
package com.dh7789dev.xpeditis.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entité JPA pour les méthodes de paiement Stripe
|
||||
*/
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@FieldNameConstants
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@Table(name = "payment_methods")
|
||||
@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class)
|
||||
public class PaymentMethodEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(columnDefinition = "BINARY(16)")
|
||||
UUID id;
|
||||
|
||||
@Column(name = "stripe_payment_method_id", unique = true, nullable = false)
|
||||
String stripePaymentMethodId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "type", nullable = false, length = 50)
|
||||
PaymentMethodTypeEntity type;
|
||||
|
||||
@Column(name = "is_default", nullable = false)
|
||||
Boolean isDefault = false;
|
||||
|
||||
@Column(name = "card_brand", length = 50)
|
||||
String cardBrand;
|
||||
|
||||
@Column(name = "card_last4", length = 4)
|
||||
String cardLast4;
|
||||
|
||||
@Column(name = "card_exp_month")
|
||||
Integer cardExpMonth;
|
||||
|
||||
@Column(name = "card_exp_year")
|
||||
Integer cardExpYear;
|
||||
|
||||
@Column(name = "bank_name", length = 100)
|
||||
String bankName;
|
||||
|
||||
@Column(name = "company_id", columnDefinition = "BINARY(16)", nullable = false)
|
||||
UUID companyId;
|
||||
|
||||
@Column(name = "created_at", updatable = false)
|
||||
LocalDateTime createdAt;
|
||||
|
||||
@org.springframework.data.annotation.CreatedDate
|
||||
@Column(name = "created_date", updatable = false, nullable = false)
|
||||
java.time.Instant createdDate;
|
||||
|
||||
@org.springframework.data.annotation.LastModifiedDate
|
||||
@Column(name = "modified_date", nullable = false)
|
||||
java.time.Instant modifiedDate;
|
||||
|
||||
@org.springframework.data.annotation.CreatedBy
|
||||
@Column(name = "created_by", updatable = false, nullable = false)
|
||||
String createdBy = "SYSTEM";
|
||||
|
||||
@org.springframework.data.annotation.LastModifiedBy
|
||||
@Column(name = "modified_by")
|
||||
String modifiedBy;
|
||||
|
||||
@PrePersist
|
||||
public void onCreate() {
|
||||
if (id == null) {
|
||||
id = UUID.randomUUID();
|
||||
}
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est une carte de crédit/débit
|
||||
*/
|
||||
public boolean isCard() {
|
||||
return type == PaymentMethodTypeEntity.CARD;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un prélèvement bancaire SEPA
|
||||
*/
|
||||
public boolean isSepaDebit() {
|
||||
return type == PaymentMethodTypeEntity.SEPA_DEBIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette méthode de paiement est la méthode par défaut
|
||||
*/
|
||||
public boolean isDefaultPaymentMethod() {
|
||||
return isDefault != null && isDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la carte expire bientôt (dans les 2 prochains mois)
|
||||
*/
|
||||
public boolean isCardExpiringSoon() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expirationDate = LocalDateTime.of(cardExpYear, cardExpMonth, 1, 0, 0)
|
||||
.plusMonths(1) // Le dernier jour du mois d'expiration
|
||||
.minusDays(1);
|
||||
|
||||
LocalDateTime twoMonthsFromNow = now.plusMonths(2);
|
||||
return expirationDate.isBefore(twoMonthsFromNow);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la carte est expirée
|
||||
*/
|
||||
public boolean isCardExpired() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expirationDate = LocalDateTime.of(cardExpYear, cardExpMonth, 1, 0, 0)
|
||||
.plusMonths(1)
|
||||
.minusDays(1);
|
||||
|
||||
return expirationDate.isBefore(now);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours jusqu'à l'expiration (négatif si déjà expirée)
|
||||
*/
|
||||
public long getDaysUntilExpiration() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expirationDate = LocalDateTime.of(cardExpYear, cardExpMonth, 1, 0, 0)
|
||||
.plusMonths(1)
|
||||
.minusDays(1);
|
||||
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(now, expirationDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return une description formatée de la méthode de paiement
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
if (isCard()) {
|
||||
String brand = cardBrand != null ? cardBrand.toUpperCase() : "CARD";
|
||||
String last4 = cardLast4 != null ? cardLast4 : "****";
|
||||
return brand + " •••• " + last4;
|
||||
} else if (isSepaDebit()) {
|
||||
String bank = bankName != null ? bankName : "Bank";
|
||||
return "SEPA - " + bank;
|
||||
} else {
|
||||
return type.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return les détails d'expiration formatés (MM/YY)
|
||||
*/
|
||||
public String getFormattedExpiration() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return String.format("%02d/%02d", cardExpMonth, cardExpYear % 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette méthode de paiement peut être utilisée pour des paiements récurrents
|
||||
*/
|
||||
public boolean supportsRecurringPayments() {
|
||||
return switch (type) {
|
||||
case CARD, SEPA_DEBIT -> true;
|
||||
case BANCONTACT, GIROPAY, IDEAL -> false; // Ces méthodes ne supportent généralement pas les paiements récurrents
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette méthode de paiement nécessite une attention (expirée ou expirant bientôt)
|
||||
*/
|
||||
public boolean requiresAttention() {
|
||||
return isCardExpired() || isCardExpiringSoon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque cette méthode comme méthode par défaut
|
||||
*/
|
||||
public void setAsDefault() {
|
||||
this.isDefault = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retire le statut de méthode par défaut
|
||||
*/
|
||||
public void removeAsDefault() {
|
||||
this.isDefault = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return l'icône à afficher pour cette méthode de paiement
|
||||
*/
|
||||
public String getIconName() {
|
||||
if (isCard() && cardBrand != null) {
|
||||
return switch (cardBrand.toLowerCase()) {
|
||||
case "visa" -> "visa";
|
||||
case "mastercard" -> "mastercard";
|
||||
case "amex", "american_express" -> "amex";
|
||||
case "discover" -> "discover";
|
||||
case "diners", "diners_club" -> "diners";
|
||||
case "jcb" -> "jcb";
|
||||
case "unionpay" -> "unionpay";
|
||||
default -> "card";
|
||||
};
|
||||
} else if (isSepaDebit()) {
|
||||
return "bank";
|
||||
} else {
|
||||
return switch (type) {
|
||||
case BANCONTACT -> "bancontact";
|
||||
case GIROPAY -> "giropay";
|
||||
case IDEAL -> "ideal";
|
||||
default -> "payment";
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Énumération des types de méthodes de paiement
|
||||
*/
|
||||
enum PaymentMethodTypeEntity {
|
||||
CARD, // Carte de crédit/débit
|
||||
SEPA_DEBIT, // Prélèvement SEPA
|
||||
BANCONTACT, // Bancontact (Belgique)
|
||||
GIROPAY, // Giropay (Allemagne)
|
||||
IDEAL, // iDEAL (Pays-Bas)
|
||||
SOFORT, // Sofort (Europe)
|
||||
P24, // Przelewy24 (Pologne)
|
||||
EPS, // EPS (Autriche)
|
||||
FPX, // FPX (Malaisie)
|
||||
BACS_DEBIT // Prélèvement BACS (Royaume-Uni)
|
||||
}
|
||||
package com.dh7789dev.xpeditis.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entité JPA pour les méthodes de paiement Stripe
|
||||
*/
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@FieldNameConstants
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@Table(name = "payment_methods")
|
||||
@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class)
|
||||
public class PaymentMethodEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(columnDefinition = "BINARY(16)")
|
||||
UUID id;
|
||||
|
||||
@Column(name = "stripe_payment_method_id", unique = true, nullable = false)
|
||||
String stripePaymentMethodId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "type", nullable = false, length = 50)
|
||||
PaymentMethodTypeEntity type;
|
||||
|
||||
@Column(name = "is_default", nullable = false)
|
||||
Boolean isDefault = false;
|
||||
|
||||
@Column(name = "card_brand", length = 50)
|
||||
String cardBrand;
|
||||
|
||||
@Column(name = "card_last4", length = 4)
|
||||
String cardLast4;
|
||||
|
||||
@Column(name = "card_exp_month")
|
||||
Integer cardExpMonth;
|
||||
|
||||
@Column(name = "card_exp_year")
|
||||
Integer cardExpYear;
|
||||
|
||||
@Column(name = "bank_name", length = 100)
|
||||
String bankName;
|
||||
|
||||
@Column(name = "company_id", columnDefinition = "BINARY(16)", nullable = false)
|
||||
UUID companyId;
|
||||
|
||||
@Column(name = "created_at", updatable = false)
|
||||
LocalDateTime createdAt;
|
||||
|
||||
@org.springframework.data.annotation.CreatedDate
|
||||
@Column(name = "created_date", updatable = false, nullable = false)
|
||||
java.time.Instant createdDate;
|
||||
|
||||
@org.springframework.data.annotation.LastModifiedDate
|
||||
@Column(name = "modified_date", nullable = false)
|
||||
java.time.Instant modifiedDate;
|
||||
|
||||
@org.springframework.data.annotation.CreatedBy
|
||||
@Column(name = "created_by", updatable = false, nullable = false)
|
||||
String createdBy = "SYSTEM";
|
||||
|
||||
@org.springframework.data.annotation.LastModifiedBy
|
||||
@Column(name = "modified_by")
|
||||
String modifiedBy;
|
||||
|
||||
@PrePersist
|
||||
public void onCreate() {
|
||||
if (id == null) {
|
||||
id = UUID.randomUUID();
|
||||
}
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est une carte de crédit/débit
|
||||
*/
|
||||
public boolean isCard() {
|
||||
return type == PaymentMethodTypeEntity.CARD;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un prélèvement bancaire SEPA
|
||||
*/
|
||||
public boolean isSepaDebit() {
|
||||
return type == PaymentMethodTypeEntity.SEPA_DEBIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette méthode de paiement est la méthode par défaut
|
||||
*/
|
||||
public boolean isDefaultPaymentMethod() {
|
||||
return isDefault != null && isDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la carte expire bientôt (dans les 2 prochains mois)
|
||||
*/
|
||||
public boolean isCardExpiringSoon() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expirationDate = LocalDateTime.of(cardExpYear, cardExpMonth, 1, 0, 0)
|
||||
.plusMonths(1) // Le dernier jour du mois d'expiration
|
||||
.minusDays(1);
|
||||
|
||||
LocalDateTime twoMonthsFromNow = now.plusMonths(2);
|
||||
return expirationDate.isBefore(twoMonthsFromNow);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la carte est expirée
|
||||
*/
|
||||
public boolean isCardExpired() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expirationDate = LocalDateTime.of(cardExpYear, cardExpMonth, 1, 0, 0)
|
||||
.plusMonths(1)
|
||||
.minusDays(1);
|
||||
|
||||
return expirationDate.isBefore(now);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours jusqu'à l'expiration (négatif si déjà expirée)
|
||||
*/
|
||||
public long getDaysUntilExpiration() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expirationDate = LocalDateTime.of(cardExpYear, cardExpMonth, 1, 0, 0)
|
||||
.plusMonths(1)
|
||||
.minusDays(1);
|
||||
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(now, expirationDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return une description formatée de la méthode de paiement
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
if (isCard()) {
|
||||
String brand = cardBrand != null ? cardBrand.toUpperCase() : "CARD";
|
||||
String last4 = cardLast4 != null ? cardLast4 : "****";
|
||||
return brand + " •••• " + last4;
|
||||
} else if (isSepaDebit()) {
|
||||
String bank = bankName != null ? bankName : "Bank";
|
||||
return "SEPA - " + bank;
|
||||
} else {
|
||||
return type.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return les détails d'expiration formatés (MM/YY)
|
||||
*/
|
||||
public String getFormattedExpiration() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return String.format("%02d/%02d", cardExpMonth, cardExpYear % 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette méthode de paiement peut être utilisée pour des paiements récurrents
|
||||
*/
|
||||
public boolean supportsRecurringPayments() {
|
||||
return switch (type) {
|
||||
case CARD, SEPA_DEBIT -> true;
|
||||
case BANCONTACT, GIROPAY, IDEAL -> false; // Ces méthodes ne supportent généralement pas les paiements récurrents
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette méthode de paiement nécessite une attention (expirée ou expirant bientôt)
|
||||
*/
|
||||
public boolean requiresAttention() {
|
||||
return isCardExpired() || isCardExpiringSoon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque cette méthode comme méthode par défaut
|
||||
*/
|
||||
public void setAsDefault() {
|
||||
this.isDefault = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retire le statut de méthode par défaut
|
||||
*/
|
||||
public void removeAsDefault() {
|
||||
this.isDefault = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return l'icône à afficher pour cette méthode de paiement
|
||||
*/
|
||||
public String getIconName() {
|
||||
if (isCard() && cardBrand != null) {
|
||||
return switch (cardBrand.toLowerCase()) {
|
||||
case "visa" -> "visa";
|
||||
case "mastercard" -> "mastercard";
|
||||
case "amex", "american_express" -> "amex";
|
||||
case "discover" -> "discover";
|
||||
case "diners", "diners_club" -> "diners";
|
||||
case "jcb" -> "jcb";
|
||||
case "unionpay" -> "unionpay";
|
||||
default -> "card";
|
||||
};
|
||||
} else if (isSepaDebit()) {
|
||||
return "bank";
|
||||
} else {
|
||||
return switch (type) {
|
||||
case BANCONTACT -> "bancontact";
|
||||
case GIROPAY -> "giropay";
|
||||
case IDEAL -> "ideal";
|
||||
default -> "payment";
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
package com.dh7789dev.xpeditis.entity;
|
||||
|
||||
/**
|
||||
* Énumération des types de méthodes de paiement
|
||||
*/
|
||||
public enum PaymentMethodTypeEntity {
|
||||
CARD, // Carte de crédit/débit
|
||||
SEPA_DEBIT, // Prélèvement SEPA
|
||||
BANCONTACT, // Bancontact (Belgique)
|
||||
GIROPAY, // Giropay (Allemagne)
|
||||
IDEAL, // iDEAL (Pays-Bas)
|
||||
SOFORT, // Sofort (Europe)
|
||||
P24, // Przelewy24 (Pologne)
|
||||
EPS, // EPS (Autriche)
|
||||
FPX, // FPX (Malaisie)
|
||||
BACS_DEBIT // Prélèvement BACS (Royaume-Uni)
|
||||
}
|
||||
@ -62,7 +62,8 @@ public class SubscriptionEntity {
|
||||
@Column(name = "trial_end_date")
|
||||
LocalDateTime trialEndDate;
|
||||
|
||||
@OneToOne(mappedBy = "subscription", cascade = CascadeType.ALL)
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "license_id")
|
||||
LicenseEntity license;
|
||||
|
||||
@OneToMany(mappedBy = "subscription", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
|
||||
@ -1,134 +0,0 @@
|
||||
package com.dh7789dev.xpeditis.mapper;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.InvoiceLineItem;
|
||||
import com.dh7789dev.xpeditis.entity.InvoiceLineItemEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Mapper entre InvoiceLineItem (domaine) et InvoiceLineItemEntity (JPA)
|
||||
*/
|
||||
@Component
|
||||
public class InvoiceLineItemMapper {
|
||||
|
||||
/**
|
||||
* Convertit une entité JPA en objet domaine
|
||||
*/
|
||||
public InvoiceLineItem toDomain(InvoiceLineItemEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InvoiceLineItem lineItem = new InvoiceLineItem();
|
||||
lineItem.setId(entity.getId());
|
||||
lineItem.setDescription(entity.getDescription());
|
||||
lineItem.setQuantity(entity.getQuantity());
|
||||
lineItem.setUnitPrice(entity.getUnitPrice());
|
||||
lineItem.setAmount(entity.getAmount());
|
||||
lineItem.setStripePriceId(entity.getStripePriceId());
|
||||
lineItem.setPeriodStart(entity.getPeriodStart());
|
||||
lineItem.setPeriodEnd(entity.getPeriodEnd());
|
||||
lineItem.setProrated(entity.getProrated());
|
||||
lineItem.setCreatedAt(entity.getCreatedAt());
|
||||
|
||||
// L'ID de la facture sera défini par le mapper parent
|
||||
if (entity.getInvoice() != null) {
|
||||
lineItem.setInvoiceId(entity.getInvoice().getId());
|
||||
}
|
||||
|
||||
return lineItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un objet domaine en entité JPA
|
||||
*/
|
||||
public InvoiceLineItemEntity toEntity(InvoiceLineItem domain) {
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InvoiceLineItemEntity entity = new InvoiceLineItemEntity();
|
||||
entity.setId(domain.getId());
|
||||
entity.setDescription(domain.getDescription());
|
||||
entity.setQuantity(domain.getQuantity());
|
||||
entity.setUnitPrice(domain.getUnitPrice());
|
||||
entity.setAmount(domain.getAmount());
|
||||
entity.setStripePriceId(domain.getStripePriceId());
|
||||
entity.setPeriodStart(domain.getPeriodStart());
|
||||
entity.setPeriodEnd(domain.getPeriodEnd());
|
||||
entity.setProrated(domain.getProrated());
|
||||
entity.setCreatedAt(domain.getCreatedAt());
|
||||
|
||||
// La relation avec la facture sera définie séparément
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une entité existante avec les données du domaine
|
||||
*/
|
||||
public void updateEntity(InvoiceLineItemEntity entity, InvoiceLineItem domain) {
|
||||
if (entity == null || domain == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
entity.setDescription(domain.getDescription());
|
||||
entity.setQuantity(domain.getQuantity());
|
||||
entity.setUnitPrice(domain.getUnitPrice());
|
||||
entity.setAmount(domain.getAmount());
|
||||
entity.setStripePriceId(domain.getStripePriceId());
|
||||
entity.setPeriodStart(domain.getPeriodStart());
|
||||
entity.setPeriodEnd(domain.getPeriodEnd());
|
||||
entity.setProrated(domain.getProrated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une liste d'entités en liste d'objets domaine
|
||||
*/
|
||||
public List<InvoiceLineItem> toDomainList(List<InvoiceLineItemEntity> entities) {
|
||||
if (entities == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return entities.stream()
|
||||
.map(this::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une liste d'objets domaine en liste d'entités
|
||||
*/
|
||||
public List<InvoiceLineItemEntity> toEntityList(List<InvoiceLineItem> domains) {
|
||||
if (domains == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return domains.stream()
|
||||
.map(this::toEntity)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une ligne de facture minimale (sans dates de période)
|
||||
*/
|
||||
public InvoiceLineItem toDomainMinimal(InvoiceLineItemEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InvoiceLineItem lineItem = new InvoiceLineItem();
|
||||
lineItem.setId(entity.getId());
|
||||
lineItem.setDescription(entity.getDescription());
|
||||
lineItem.setQuantity(entity.getQuantity());
|
||||
lineItem.setAmount(entity.getAmount());
|
||||
lineItem.setProrated(entity.getProrated());
|
||||
|
||||
if (entity.getInvoice() != null) {
|
||||
lineItem.setInvoiceId(entity.getInvoice().getId());
|
||||
}
|
||||
|
||||
return lineItem;
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,9 @@
|
||||
package com.dh7789dev.xpeditis.mapper;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.Invoice;
|
||||
import com.dh7789dev.xpeditis.dto.app.InvoiceLineItem;
|
||||
import com.dh7789dev.xpeditis.dto.app.InvoiceStatus;
|
||||
import com.dh7789dev.xpeditis.entity.InvoiceEntity;
|
||||
import com.dh7789dev.xpeditis.entity.InvoiceStatusEntity;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
@ -13,19 +11,11 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Mapper entre Invoice (domaine) et InvoiceEntity (JPA)
|
||||
* Version simplifiée sans relations complexes
|
||||
*/
|
||||
@Component
|
||||
public class InvoiceMapper {
|
||||
|
||||
private final SubscriptionMapper subscriptionMapper;
|
||||
private final InvoiceLineItemMapper lineItemMapper;
|
||||
|
||||
@Autowired
|
||||
public InvoiceMapper(SubscriptionMapper subscriptionMapper, InvoiceLineItemMapper lineItemMapper) {
|
||||
this.subscriptionMapper = subscriptionMapper;
|
||||
this.lineItemMapper = lineItemMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une entité JPA en objet domaine
|
||||
*/
|
||||
@ -38,7 +28,12 @@ public class InvoiceMapper {
|
||||
invoice.setId(entity.getId());
|
||||
invoice.setStripeInvoiceId(entity.getStripeInvoiceId());
|
||||
invoice.setInvoiceNumber(entity.getInvoiceNumber());
|
||||
invoice.setStatus(mapStatusToDomain(entity.getStatus()));
|
||||
|
||||
// Conversion enum status
|
||||
if (entity.getStatus() != null) {
|
||||
invoice.setStatus(InvoiceStatus.valueOf(entity.getStatus().name()));
|
||||
}
|
||||
|
||||
invoice.setAmountDue(entity.getAmountDue());
|
||||
invoice.setAmountPaid(entity.getAmountPaid());
|
||||
invoice.setCurrency(entity.getCurrency());
|
||||
@ -51,25 +46,7 @@ public class InvoiceMapper {
|
||||
invoice.setAttemptCount(entity.getAttemptCount());
|
||||
invoice.setCreatedAt(entity.getCreatedAt());
|
||||
|
||||
// Mapping des relations (attention aux cycles)
|
||||
if (entity.getSubscription() != null) {
|
||||
// On ne mappe pas la subscription complète pour éviter les cycles infinis
|
||||
// Elle sera chargée séparément si nécessaire
|
||||
invoice.setSubscriptionId(entity.getSubscription().getId());
|
||||
}
|
||||
|
||||
// Mapping des lignes de facture
|
||||
if (entity.getLineItems() != null && !entity.getLineItems().isEmpty()) {
|
||||
List<InvoiceLineItem> lineItems = entity.getLineItems().stream()
|
||||
.map(lineItemEntity -> {
|
||||
InvoiceLineItem lineItem = lineItemMapper.toDomain(lineItemEntity);
|
||||
// Éviter le cycle en définissant la référence à la facture
|
||||
lineItem.setInvoiceId(invoice.getId());
|
||||
return lineItem;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
invoice.setLineItems(lineItems);
|
||||
}
|
||||
// Subscription et LineItems seront chargés séparément si nécessaire
|
||||
|
||||
return invoice;
|
||||
}
|
||||
@ -86,7 +63,12 @@ public class InvoiceMapper {
|
||||
entity.setId(domain.getId());
|
||||
entity.setStripeInvoiceId(domain.getStripeInvoiceId());
|
||||
entity.setInvoiceNumber(domain.getInvoiceNumber());
|
||||
entity.setStatus(mapStatusToEntity(domain.getStatus()));
|
||||
|
||||
// Conversion enum status
|
||||
if (domain.getStatus() != null) {
|
||||
entity.setStatus(InvoiceStatusEntity.valueOf(domain.getStatus().name()));
|
||||
}
|
||||
|
||||
entity.setAmountDue(domain.getAmountDue());
|
||||
entity.setAmountPaid(domain.getAmountPaid());
|
||||
entity.setCurrency(domain.getCurrency());
|
||||
@ -99,8 +81,6 @@ public class InvoiceMapper {
|
||||
entity.setAttemptCount(domain.getAttemptCount());
|
||||
entity.setCreatedAt(domain.getCreatedAt());
|
||||
|
||||
// Les relations seront définies séparément
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
@ -112,8 +92,13 @@ public class InvoiceMapper {
|
||||
return;
|
||||
}
|
||||
|
||||
entity.setStripeInvoiceId(domain.getStripeInvoiceId());
|
||||
entity.setInvoiceNumber(domain.getInvoiceNumber());
|
||||
entity.setStatus(mapStatusToEntity(domain.getStatus()));
|
||||
|
||||
if (domain.getStatus() != null) {
|
||||
entity.setStatus(InvoiceStatusEntity.valueOf(domain.getStatus().name()));
|
||||
}
|
||||
|
||||
entity.setAmountDue(domain.getAmountDue());
|
||||
entity.setAmountPaid(domain.getAmountPaid());
|
||||
entity.setCurrency(domain.getCurrency());
|
||||
@ -151,89 +136,4 @@ public class InvoiceMapper {
|
||||
.map(this::toEntity)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une facture domaine minimale (sans lignes de facture)
|
||||
*/
|
||||
public Invoice toDomainMinimal(InvoiceEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Invoice invoice = new Invoice();
|
||||
invoice.setId(entity.getId());
|
||||
invoice.setStripeInvoiceId(entity.getStripeInvoiceId());
|
||||
invoice.setInvoiceNumber(entity.getInvoiceNumber());
|
||||
invoice.setStatus(mapStatusToDomain(entity.getStatus()));
|
||||
invoice.setAmountDue(entity.getAmountDue());
|
||||
invoice.setAmountPaid(entity.getAmountPaid());
|
||||
invoice.setCurrency(entity.getCurrency());
|
||||
invoice.setDueDate(entity.getDueDate());
|
||||
invoice.setPaidAt(entity.getPaidAt());
|
||||
invoice.setAttemptCount(entity.getAttemptCount());
|
||||
invoice.setCreatedAt(entity.getCreatedAt());
|
||||
|
||||
if (entity.getSubscription() != null) {
|
||||
invoice.setSubscriptionId(entity.getSubscription().getId());
|
||||
}
|
||||
|
||||
return invoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour seulement le statut de paiement
|
||||
*/
|
||||
public void updatePaymentStatus(InvoiceEntity entity, InvoiceStatus status, java.time.LocalDateTime paidAt, java.math.BigDecimal amountPaid) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
entity.setStatus(mapStatusToEntity(status));
|
||||
entity.setPaidAt(paidAt);
|
||||
entity.setAmountPaid(amountPaid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrémente le nombre de tentatives
|
||||
*/
|
||||
public void incrementAttemptCount(InvoiceEntity entity) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Integer currentCount = entity.getAttemptCount();
|
||||
entity.setAttemptCount(currentCount != null ? currentCount + 1 : 1);
|
||||
}
|
||||
|
||||
// ===== MÉTHODES DE MAPPING DES ÉNUMÉRATIONS =====
|
||||
|
||||
private InvoiceStatus mapStatusToDomain(InvoiceStatusEntity status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (status) {
|
||||
case DRAFT -> InvoiceStatus.DRAFT;
|
||||
case OPEN -> InvoiceStatus.OPEN;
|
||||
case PAID -> InvoiceStatus.PAID;
|
||||
case PAYMENT_FAILED -> InvoiceStatus.PAYMENT_FAILED;
|
||||
case VOIDED -> InvoiceStatus.VOIDED;
|
||||
case UNCOLLECTIBLE -> InvoiceStatus.UNCOLLECTIBLE;
|
||||
};
|
||||
}
|
||||
|
||||
private InvoiceStatusEntity mapStatusToEntity(InvoiceStatus status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (status) {
|
||||
case DRAFT -> InvoiceStatusEntity.DRAFT;
|
||||
case OPEN -> InvoiceStatusEntity.OPEN;
|
||||
case PAID -> InvoiceStatusEntity.PAID;
|
||||
case PAYMENT_FAILED -> InvoiceStatusEntity.PAYMENT_FAILED;
|
||||
case VOIDED -> InvoiceStatusEntity.VOIDED;
|
||||
case UNCOLLECTIBLE -> InvoiceStatusEntity.UNCOLLECTIBLE;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,156 +0,0 @@
|
||||
package com.dh7789dev.xpeditis.mapper;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.EventStatus;
|
||||
import com.dh7789dev.xpeditis.dto.app.PaymentEvent;
|
||||
import com.dh7789dev.xpeditis.entity.PaymentEventEntity;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Mapper entre PaymentEvent (domaine) et PaymentEventEntity (JPA)
|
||||
*/
|
||||
@Component
|
||||
public class PaymentEventMapper {
|
||||
|
||||
private final SubscriptionMapper subscriptionMapper;
|
||||
|
||||
@Autowired
|
||||
public PaymentEventMapper(SubscriptionMapper subscriptionMapper) {
|
||||
this.subscriptionMapper = subscriptionMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une entité JPA en objet domaine
|
||||
*/
|
||||
public PaymentEvent toDomain(PaymentEventEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PaymentEvent event = new PaymentEvent();
|
||||
event.setId(entity.getId());
|
||||
event.setStripeEventId(entity.getStripeEventId());
|
||||
event.setEventType(entity.getEventType());
|
||||
event.setStatus(entity.getStatus());
|
||||
event.setPayload(entity.getPayload());
|
||||
event.setProcessedAt(entity.getProcessedAt());
|
||||
event.setErrorMessage(entity.getErrorMessage());
|
||||
event.setRetryCount(entity.getRetryCount());
|
||||
event.setCreatedAt(entity.getCreatedAt());
|
||||
|
||||
// Mapping de la relation subscription (sans cycle infini)
|
||||
if (entity.getSubscription() != null) {
|
||||
event.setSubscriptionId(entity.getSubscription().getId());
|
||||
// On ne mappe pas la subscription complète ici pour éviter les cycles
|
||||
// Si nécessaire, elle sera chargée séparément
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un objet domaine en entité JPA
|
||||
*/
|
||||
public PaymentEventEntity toEntity(PaymentEvent domain) {
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PaymentEventEntity entity = new PaymentEventEntity();
|
||||
entity.setId(domain.getId());
|
||||
entity.setStripeEventId(domain.getStripeEventId());
|
||||
entity.setEventType(domain.getEventType());
|
||||
entity.setStatus(domain.getStatus());
|
||||
entity.setPayload(domain.getPayload());
|
||||
entity.setProcessedAt(domain.getProcessedAt());
|
||||
entity.setErrorMessage(domain.getErrorMessage());
|
||||
entity.setRetryCount(domain.getRetryCount());
|
||||
entity.setCreatedAt(domain.getCreatedAt());
|
||||
|
||||
// La relation subscription sera définie séparément
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une entité existante avec les données du domaine
|
||||
*/
|
||||
public void updateEntity(PaymentEventEntity entity, PaymentEvent domain) {
|
||||
if (entity == null || domain == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
entity.setEventType(domain.getEventType());
|
||||
entity.setStatus(domain.getStatus());
|
||||
entity.setPayload(domain.getPayload());
|
||||
entity.setProcessedAt(domain.getProcessedAt());
|
||||
entity.setErrorMessage(domain.getErrorMessage());
|
||||
entity.setRetryCount(domain.getRetryCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une liste d'entités en liste d'objets domaine
|
||||
*/
|
||||
public List<PaymentEvent> toDomainList(List<PaymentEventEntity> entities) {
|
||||
if (entities == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return entities.stream()
|
||||
.map(this::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une liste d'objets domaine en liste d'entités
|
||||
*/
|
||||
public List<PaymentEventEntity> toEntityList(List<PaymentEvent> domains) {
|
||||
if (domains == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return domains.stream()
|
||||
.map(this::toEntity)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un événement domaine minimal (pour les cas où on n'a besoin que des infos de base)
|
||||
*/
|
||||
public PaymentEvent toDomainMinimal(PaymentEventEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PaymentEvent event = new PaymentEvent();
|
||||
event.setId(entity.getId());
|
||||
event.setStripeEventId(entity.getStripeEventId());
|
||||
event.setEventType(entity.getEventType());
|
||||
event.setStatus(entity.getStatus());
|
||||
event.setRetryCount(entity.getRetryCount());
|
||||
event.setCreatedAt(entity.getCreatedAt());
|
||||
event.setProcessedAt(entity.getProcessedAt());
|
||||
event.setErrorMessage(entity.getErrorMessage());
|
||||
|
||||
// Pas de payload ni de relations pour une version minimale
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappe seulement les données de statut (pour les mises à jour rapides)
|
||||
*/
|
||||
public void updateStatusOnly(PaymentEventEntity entity, PaymentEvent domain) {
|
||||
if (entity == null || domain == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
entity.setStatus(domain.getStatus());
|
||||
entity.setProcessedAt(domain.getProcessedAt());
|
||||
entity.setErrorMessage(domain.getErrorMessage());
|
||||
entity.setRetryCount(domain.getRetryCount());
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
package com.dh7789dev.xpeditis.mapper;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.PaymentMethod;
|
||||
import com.dh7789dev.xpeditis.dto.app.PaymentMethodType;
|
||||
import com.dh7789dev.xpeditis.dto.app.PaymentType;
|
||||
import com.dh7789dev.xpeditis.entity.PaymentMethodEntity;
|
||||
import com.dh7789dev.xpeditis.entity.PaymentMethodTypeEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
@ -11,6 +11,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Mapper entre PaymentMethod (domaine) et PaymentMethodEntity (JPA)
|
||||
* Version simplifiée sans relation Company complexe
|
||||
*/
|
||||
@Component
|
||||
public class PaymentMethodMapper {
|
||||
@ -26,16 +27,22 @@ public class PaymentMethodMapper {
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.setId(entity.getId());
|
||||
paymentMethod.setStripePaymentMethodId(entity.getStripePaymentMethodId());
|
||||
paymentMethod.setType(mapTypeToDomain(entity.getType()));
|
||||
paymentMethod.setIsDefault(entity.getIsDefault());
|
||||
|
||||
// Conversion enum type
|
||||
if (entity.getType() != null) {
|
||||
paymentMethod.setType(PaymentType.valueOf(entity.getType().name()));
|
||||
}
|
||||
|
||||
paymentMethod.setDefault(entity.getIsDefault() != null && entity.getIsDefault());
|
||||
paymentMethod.setCardBrand(entity.getCardBrand());
|
||||
paymentMethod.setCardLast4(entity.getCardLast4());
|
||||
paymentMethod.setCardExpMonth(entity.getCardExpMonth());
|
||||
paymentMethod.setCardExpYear(entity.getCardExpYear());
|
||||
paymentMethod.setBankName(entity.getBankName());
|
||||
paymentMethod.setCompanyId(entity.getCompanyId());
|
||||
paymentMethod.setCreatedAt(entity.getCreatedAt());
|
||||
|
||||
// Company sera chargée séparément si nécessaire
|
||||
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
@ -50,16 +57,22 @@ public class PaymentMethodMapper {
|
||||
PaymentMethodEntity entity = new PaymentMethodEntity();
|
||||
entity.setId(domain.getId());
|
||||
entity.setStripePaymentMethodId(domain.getStripePaymentMethodId());
|
||||
entity.setType(mapTypeToEntity(domain.getType()));
|
||||
entity.setIsDefault(domain.getIsDefault());
|
||||
|
||||
// Conversion enum type
|
||||
if (domain.getType() != null) {
|
||||
entity.setType(PaymentMethodTypeEntity.valueOf(domain.getType().name()));
|
||||
}
|
||||
|
||||
entity.setIsDefault(domain.isDefault());
|
||||
entity.setCardBrand(domain.getCardBrand());
|
||||
entity.setCardLast4(domain.getCardLast4());
|
||||
entity.setCardExpMonth(domain.getCardExpMonth());
|
||||
entity.setCardExpYear(domain.getCardExpYear());
|
||||
entity.setBankName(domain.getBankName());
|
||||
entity.setCompanyId(domain.getCompanyId());
|
||||
entity.setCreatedAt(domain.getCreatedAt());
|
||||
|
||||
// companyId sera défini par le service
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
@ -71,8 +84,13 @@ public class PaymentMethodMapper {
|
||||
return;
|
||||
}
|
||||
|
||||
entity.setType(mapTypeToEntity(domain.getType()));
|
||||
entity.setIsDefault(domain.getIsDefault());
|
||||
entity.setStripePaymentMethodId(domain.getStripePaymentMethodId());
|
||||
|
||||
if (domain.getType() != null) {
|
||||
entity.setType(PaymentMethodTypeEntity.valueOf(domain.getType().name()));
|
||||
}
|
||||
|
||||
entity.setIsDefault(domain.isDefault());
|
||||
entity.setCardBrand(domain.getCardBrand());
|
||||
entity.setCardLast4(domain.getCardLast4());
|
||||
entity.setCardExpMonth(domain.getCardExpMonth());
|
||||
@ -105,91 +123,4 @@ public class PaymentMethodMapper {
|
||||
.map(this::toEntity)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une méthode de paiement minimale (sans détails sensibles)
|
||||
*/
|
||||
public PaymentMethod toDomainMinimal(PaymentMethodEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.setId(entity.getId());
|
||||
paymentMethod.setType(mapTypeToDomain(entity.getType()));
|
||||
paymentMethod.setIsDefault(entity.getIsDefault());
|
||||
paymentMethod.setCardBrand(entity.getCardBrand());
|
||||
paymentMethod.setCardLast4(entity.getCardLast4());
|
||||
paymentMethod.setCreatedAt(entity.getCreatedAt());
|
||||
paymentMethod.setCompanyId(entity.getCompanyId());
|
||||
|
||||
// Pas d'informations sensibles comme les dates d'expiration détaillées
|
||||
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour seulement le statut par défaut
|
||||
*/
|
||||
public void updateDefaultStatus(PaymentMethodEntity entity, boolean isDefault) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
entity.setIsDefault(isDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les informations de carte (suite à une mise à jour Stripe)
|
||||
*/
|
||||
public void updateCardInfo(PaymentMethodEntity entity, String cardBrand, String cardLast4, Integer expMonth, Integer expYear) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
entity.setCardBrand(cardBrand);
|
||||
entity.setCardLast4(cardLast4);
|
||||
entity.setCardExpMonth(expMonth);
|
||||
entity.setCardExpYear(expYear);
|
||||
}
|
||||
|
||||
// ===== MÉTHODES DE MAPPING DES ÉNUMÉRATIONS =====
|
||||
|
||||
private PaymentMethodType mapTypeToDomain(PaymentMethodTypeEntity type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (type) {
|
||||
case CARD -> PaymentMethodType.CARD;
|
||||
case SEPA_DEBIT -> PaymentMethodType.SEPA_DEBIT;
|
||||
case BANCONTACT -> PaymentMethodType.BANCONTACT;
|
||||
case GIROPAY -> PaymentMethodType.GIROPAY;
|
||||
case IDEAL -> PaymentMethodType.IDEAL;
|
||||
case SOFORT -> PaymentMethodType.SOFORT;
|
||||
case P24 -> PaymentMethodType.P24;
|
||||
case EPS -> PaymentMethodType.EPS;
|
||||
case FPX -> PaymentMethodType.FPX;
|
||||
case BACS_DEBIT -> PaymentMethodType.BACS_DEBIT;
|
||||
};
|
||||
}
|
||||
|
||||
private PaymentMethodTypeEntity mapTypeToEntity(PaymentMethodType type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (type) {
|
||||
case CARD -> PaymentMethodTypeEntity.CARD;
|
||||
case SEPA_DEBIT -> PaymentMethodTypeEntity.SEPA_DEBIT;
|
||||
case BANCONTACT -> PaymentMethodTypeEntity.BANCONTACT;
|
||||
case GIROPAY -> PaymentMethodTypeEntity.GIROPAY;
|
||||
case IDEAL -> PaymentMethodTypeEntity.IDEAL;
|
||||
case SOFORT -> PaymentMethodTypeEntity.SOFORT;
|
||||
case P24 -> PaymentMethodTypeEntity.P24;
|
||||
case EPS -> PaymentMethodTypeEntity.EPS;
|
||||
case FPX -> PaymentMethodTypeEntity.FPX;
|
||||
case BACS_DEBIT -> PaymentMethodTypeEntity.BACS_DEBIT;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package com.dh7789dev.xpeditis.mapper;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||
import com.dh7789dev.xpeditis.dto.app.Plan;
|
||||
import com.dh7789dev.xpeditis.entity.PlanEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@ -26,7 +28,12 @@ public class PlanMapper {
|
||||
Plan plan = new Plan();
|
||||
plan.setId(entity.getId());
|
||||
plan.setName(entity.getName());
|
||||
plan.setType(entity.getType());
|
||||
|
||||
// Conversion String -> LicenseType enum
|
||||
if (entity.getType() != null) {
|
||||
plan.setType(LicenseType.valueOf(entity.getType()));
|
||||
}
|
||||
|
||||
plan.setStripePriceIdMonthly(entity.getStripePriceIdMonthly());
|
||||
plan.setStripePriceIdYearly(entity.getStripePriceIdYearly());
|
||||
plan.setMonthlyPrice(entity.getMonthlyPrice());
|
||||
@ -34,9 +41,12 @@ public class PlanMapper {
|
||||
plan.setMaxUsers(entity.getMaxUsers());
|
||||
plan.setFeatures(entity.getFeatures());
|
||||
plan.setTrialDurationDays(entity.getTrialDurationDays());
|
||||
plan.setIsActive(entity.getIsActive());
|
||||
plan.setActive(entity.getIsActive());
|
||||
plan.setDisplayOrder(entity.getDisplayOrder());
|
||||
plan.setMetadata(entity.getMetadata());
|
||||
|
||||
// Conversion Map<String,Object> -> Map<String,String>
|
||||
plan.setMetadata(convertMetadataToStringMap(entity.getMetadata()));
|
||||
|
||||
plan.setCreatedAt(entity.getCreatedAt());
|
||||
plan.setUpdatedAt(entity.getUpdatedAt());
|
||||
|
||||
@ -54,7 +64,12 @@ public class PlanMapper {
|
||||
PlanEntity entity = new PlanEntity();
|
||||
entity.setId(domain.getId());
|
||||
entity.setName(domain.getName());
|
||||
entity.setType(domain.getType());
|
||||
|
||||
// Conversion LicenseType enum -> String
|
||||
if (domain.getType() != null) {
|
||||
entity.setType(domain.getType().name());
|
||||
}
|
||||
|
||||
entity.setStripePriceIdMonthly(domain.getStripePriceIdMonthly());
|
||||
entity.setStripePriceIdYearly(domain.getStripePriceIdYearly());
|
||||
entity.setMonthlyPrice(domain.getMonthlyPrice());
|
||||
@ -62,9 +77,12 @@ public class PlanMapper {
|
||||
entity.setMaxUsers(domain.getMaxUsers());
|
||||
entity.setFeatures(domain.getFeatures());
|
||||
entity.setTrialDurationDays(domain.getTrialDurationDays());
|
||||
entity.setIsActive(domain.getIsActive());
|
||||
entity.setIsActive(domain.isActive());
|
||||
entity.setDisplayOrder(domain.getDisplayOrder());
|
||||
entity.setMetadata(domain.getMetadata());
|
||||
|
||||
// Conversion Map<String,String> -> Map<String,Object>
|
||||
entity.setMetadata(convertMetadataToObjectMap(domain.getMetadata()));
|
||||
|
||||
entity.setCreatedAt(domain.getCreatedAt());
|
||||
entity.setUpdatedAt(domain.getUpdatedAt());
|
||||
|
||||
@ -80,7 +98,11 @@ public class PlanMapper {
|
||||
}
|
||||
|
||||
entity.setName(domain.getName());
|
||||
entity.setType(domain.getType());
|
||||
|
||||
if (domain.getType() != null) {
|
||||
entity.setType(domain.getType().name());
|
||||
}
|
||||
|
||||
entity.setStripePriceIdMonthly(domain.getStripePriceIdMonthly());
|
||||
entity.setStripePriceIdYearly(domain.getStripePriceIdYearly());
|
||||
entity.setMonthlyPrice(domain.getMonthlyPrice());
|
||||
@ -88,9 +110,9 @@ public class PlanMapper {
|
||||
entity.setMaxUsers(domain.getMaxUsers());
|
||||
entity.setFeatures(domain.getFeatures());
|
||||
entity.setTrialDurationDays(domain.getTrialDurationDays());
|
||||
entity.setIsActive(domain.getIsActive());
|
||||
entity.setIsActive(domain.isActive());
|
||||
entity.setDisplayOrder(domain.getDisplayOrder());
|
||||
entity.setMetadata(domain.getMetadata());
|
||||
entity.setMetadata(convertMetadataToObjectMap(domain.getMetadata()));
|
||||
entity.setUpdatedAt(domain.getUpdatedAt());
|
||||
}
|
||||
|
||||
@ -131,14 +153,17 @@ public class PlanMapper {
|
||||
Plan plan = new Plan();
|
||||
plan.setId(entity.getId());
|
||||
plan.setName(entity.getName());
|
||||
plan.setType(entity.getType());
|
||||
|
||||
if (entity.getType() != null) {
|
||||
plan.setType(LicenseType.valueOf(entity.getType()));
|
||||
}
|
||||
|
||||
plan.setMonthlyPrice(entity.getMonthlyPrice());
|
||||
plan.setYearlyPrice(entity.getYearlyPrice());
|
||||
plan.setMaxUsers(entity.getMaxUsers());
|
||||
plan.setIsActive(entity.getIsActive());
|
||||
plan.setActive(entity.getIsActive());
|
||||
plan.setDisplayOrder(entity.getDisplayOrder());
|
||||
|
||||
// Features simplifiées (juste le count)
|
||||
if (entity.getFeatures() != null) {
|
||||
plan.setFeatures(entity.getFeatures());
|
||||
}
|
||||
@ -208,4 +233,32 @@ public class PlanMapper {
|
||||
entity.setMetadata(metadata);
|
||||
entity.setUpdatedAt(java.time.LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit Map<String,Object> vers Map<String,String>
|
||||
*/
|
||||
private Map<String, String> convertMetadataToStringMap(Map<String, Object> objectMap) {
|
||||
if (objectMap == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, String> stringMap = new HashMap<>();
|
||||
objectMap.forEach((key, value) -> {
|
||||
if (value != null) {
|
||||
stringMap.put(key, value.toString());
|
||||
}
|
||||
});
|
||||
return stringMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit Map<String,String> vers Map<String,Object>
|
||||
*/
|
||||
private Map<String, Object> convertMetadataToObjectMap(Map<String, String> stringMap) {
|
||||
if (stringMap == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new HashMap<>(stringMap);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
package com.dh7789dev.xpeditis.mapper;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.*;
|
||||
import com.dh7789dev.xpeditis.dto.app.BillingCycle;
|
||||
import com.dh7789dev.xpeditis.dto.app.Subscription;
|
||||
import com.dh7789dev.xpeditis.dto.app.SubscriptionStatus;
|
||||
import com.dh7789dev.xpeditis.entity.BillingCycleEntity;
|
||||
import com.dh7789dev.xpeditis.entity.SubscriptionEntity;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
@ -10,19 +13,11 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Mapper entre Subscription (domaine) et SubscriptionEntity (JPA)
|
||||
* Version simplifiée sans relations complexes
|
||||
*/
|
||||
@Component
|
||||
public class SubscriptionMapper {
|
||||
|
||||
private final LicenseMapper licenseMapper;
|
||||
private final InvoiceMapper invoiceMapper;
|
||||
|
||||
@Autowired
|
||||
public SubscriptionMapper(LicenseMapper licenseMapper, InvoiceMapper invoiceMapper) {
|
||||
this.licenseMapper = licenseMapper;
|
||||
this.invoiceMapper = invoiceMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une entité JPA en objet domaine
|
||||
*/
|
||||
@ -36,32 +31,27 @@ public class SubscriptionMapper {
|
||||
subscription.setStripeSubscriptionId(entity.getStripeSubscriptionId());
|
||||
subscription.setStripeCustomerId(entity.getStripeCustomerId());
|
||||
subscription.setStripePriceId(entity.getStripePriceId());
|
||||
subscription.setStatus(mapStatusToDomain(entity.getStatus()));
|
||||
|
||||
// Conversion enum status
|
||||
if (entity.getStatus() != null) {
|
||||
subscription.setStatus(SubscriptionStatus.valueOf(entity.getStatus().name()));
|
||||
}
|
||||
|
||||
subscription.setCurrentPeriodStart(entity.getCurrentPeriodStart());
|
||||
subscription.setCurrentPeriodEnd(entity.getCurrentPeriodEnd());
|
||||
subscription.setCancelAtPeriodEnd(entity.getCancelAtPeriodEnd());
|
||||
subscription.setBillingCycle(mapBillingCycleToDomain(entity.getBillingCycle()));
|
||||
subscription.setCancelAtPeriodEnd(entity.isCancelAtPeriodEnd());
|
||||
|
||||
// Conversion enum billing cycle
|
||||
if (entity.getBillingCycle() != null) {
|
||||
subscription.setBillingCycle(BillingCycle.valueOf(entity.getBillingCycle().name()));
|
||||
}
|
||||
|
||||
subscription.setNextBillingDate(entity.getNextBillingDate());
|
||||
subscription.setTrialEndDate(entity.getTrialEndDate());
|
||||
subscription.setCreatedAt(entity.getCreatedAt());
|
||||
subscription.setUpdatedAt(entity.getUpdatedAt());
|
||||
|
||||
// Mapping des relations (sans cycles infinis)
|
||||
if (entity.getLicense() != null) {
|
||||
subscription.setLicense(licenseMapper.toDomain(entity.getLicense()));
|
||||
}
|
||||
|
||||
if (entity.getInvoices() != null && !entity.getInvoices().isEmpty()) {
|
||||
List<Invoice> invoices = entity.getInvoices().stream()
|
||||
.map(invoiceEntity -> {
|
||||
Invoice invoice = invoiceMapper.toDomain(invoiceEntity);
|
||||
// Éviter le cycle infini en ne remappant pas la subscription
|
||||
invoice.setSubscription(subscription);
|
||||
return invoice;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
subscription.setInvoices(invoices);
|
||||
}
|
||||
// PaymentMethod, License et Invoices seront chargés séparément si nécessaire
|
||||
|
||||
return subscription;
|
||||
}
|
||||
@ -79,18 +69,26 @@ public class SubscriptionMapper {
|
||||
entity.setStripeSubscriptionId(domain.getStripeSubscriptionId());
|
||||
entity.setStripeCustomerId(domain.getStripeCustomerId());
|
||||
entity.setStripePriceId(domain.getStripePriceId());
|
||||
entity.setStatus(mapStatusToEntity(domain.getStatus()));
|
||||
|
||||
// Conversion enum status
|
||||
if (domain.getStatus() != null) {
|
||||
entity.setStatus(SubscriptionStatusEntity.valueOf(domain.getStatus().name()));
|
||||
}
|
||||
|
||||
entity.setCurrentPeriodStart(domain.getCurrentPeriodStart());
|
||||
entity.setCurrentPeriodEnd(domain.getCurrentPeriodEnd());
|
||||
entity.setCancelAtPeriodEnd(domain.getCancelAtPeriodEnd());
|
||||
entity.setBillingCycle(mapBillingCycleToEntity(domain.getBillingCycle()));
|
||||
entity.setCancelAtPeriodEnd(domain.isCancelAtPeriodEnd());
|
||||
|
||||
// Conversion enum billing cycle
|
||||
if (domain.getBillingCycle() != null) {
|
||||
entity.setBillingCycle(BillingCycleEntity.valueOf(domain.getBillingCycle().name()));
|
||||
}
|
||||
|
||||
entity.setNextBillingDate(domain.getNextBillingDate());
|
||||
entity.setTrialEndDate(domain.getTrialEndDate());
|
||||
entity.setCreatedAt(domain.getCreatedAt());
|
||||
entity.setUpdatedAt(domain.getUpdatedAt());
|
||||
|
||||
// Relations mappées séparément pour éviter les cycles
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
@ -105,11 +103,19 @@ public class SubscriptionMapper {
|
||||
entity.setStripeSubscriptionId(domain.getStripeSubscriptionId());
|
||||
entity.setStripeCustomerId(domain.getStripeCustomerId());
|
||||
entity.setStripePriceId(domain.getStripePriceId());
|
||||
entity.setStatus(mapStatusToEntity(domain.getStatus()));
|
||||
|
||||
if (domain.getStatus() != null) {
|
||||
entity.setStatus(SubscriptionStatusEntity.valueOf(domain.getStatus().name()));
|
||||
}
|
||||
|
||||
entity.setCurrentPeriodStart(domain.getCurrentPeriodStart());
|
||||
entity.setCurrentPeriodEnd(domain.getCurrentPeriodEnd());
|
||||
entity.setCancelAtPeriodEnd(domain.getCancelAtPeriodEnd());
|
||||
entity.setBillingCycle(mapBillingCycleToEntity(domain.getBillingCycle()));
|
||||
entity.setCancelAtPeriodEnd(domain.isCancelAtPeriodEnd());
|
||||
|
||||
if (domain.getBillingCycle() != null) {
|
||||
entity.setBillingCycle(BillingCycleEntity.valueOf(domain.getBillingCycle().name()));
|
||||
}
|
||||
|
||||
entity.setNextBillingDate(domain.getNextBillingDate());
|
||||
entity.setTrialEndDate(domain.getTrialEndDate());
|
||||
entity.setUpdatedAt(domain.getUpdatedAt());
|
||||
@ -140,62 +146,4 @@ public class SubscriptionMapper {
|
||||
.map(this::toEntity)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ===== MÉTHODES DE MAPPING DES ÉNUMÉRATIONS =====
|
||||
|
||||
private SubscriptionStatus mapStatusToDomain(com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (status) {
|
||||
case INCOMPLETE -> SubscriptionStatus.INCOMPLETE;
|
||||
case INCOMPLETE_EXPIRED -> SubscriptionStatus.INCOMPLETE_EXPIRED;
|
||||
case TRIALING -> SubscriptionStatus.TRIALING;
|
||||
case ACTIVE -> SubscriptionStatus.ACTIVE;
|
||||
case PAST_DUE -> SubscriptionStatus.PAST_DUE;
|
||||
case CANCELED -> SubscriptionStatus.CANCELED;
|
||||
case UNPAID -> SubscriptionStatus.UNPAID;
|
||||
case PAUSED -> SubscriptionStatus.PAUSED;
|
||||
};
|
||||
}
|
||||
|
||||
private com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity mapStatusToEntity(SubscriptionStatus status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (status) {
|
||||
case INCOMPLETE -> com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity.INCOMPLETE;
|
||||
case INCOMPLETE_EXPIRED -> com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity.INCOMPLETE_EXPIRED;
|
||||
case TRIALING -> com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity.TRIALING;
|
||||
case ACTIVE -> com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity.ACTIVE;
|
||||
case PAST_DUE -> com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity.PAST_DUE;
|
||||
case CANCELED -> com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity.CANCELED;
|
||||
case UNPAID -> com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity.UNPAID;
|
||||
case PAUSED -> com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity.PAUSED;
|
||||
};
|
||||
}
|
||||
|
||||
private BillingCycle mapBillingCycleToDomain(com.dh7789dev.xpeditis.entity.BillingCycleEntity billingCycle) {
|
||||
if (billingCycle == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (billingCycle) {
|
||||
case MONTHLY -> BillingCycle.MONTHLY;
|
||||
case YEARLY -> BillingCycle.YEARLY;
|
||||
};
|
||||
}
|
||||
|
||||
private com.dh7789dev.xpeditis.entity.BillingCycleEntity mapBillingCycleToEntity(BillingCycle billingCycle) {
|
||||
if (billingCycle == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (billingCycle) {
|
||||
case MONTHLY -> com.dh7789dev.xpeditis.entity.BillingCycleEntity.MONTHLY;
|
||||
case YEARLY -> com.dh7789dev.xpeditis.entity.BillingCycleEntity.YEARLY;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,15 +2,11 @@ package com.dh7789dev.xpeditis.repository;
|
||||
|
||||
import com.dh7789dev.xpeditis.entity.InvoiceEntity;
|
||||
import com.dh7789dev.xpeditis.entity.InvoiceStatusEntity;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@ -22,177 +18,38 @@ import java.util.UUID;
|
||||
@Repository
|
||||
public interface InvoiceJpaRepository extends JpaRepository<InvoiceEntity, UUID> {
|
||||
|
||||
// ===== RECHERCHES DE BASE =====
|
||||
|
||||
/**
|
||||
* Trouve une facture par son ID Stripe
|
||||
*/
|
||||
Optional<InvoiceEntity> findByStripeInvoiceId(String stripeInvoiceId);
|
||||
|
||||
/**
|
||||
* Trouve une facture par son numéro
|
||||
*/
|
||||
Optional<InvoiceEntity> findByInvoiceNumber(String invoiceNumber);
|
||||
|
||||
/**
|
||||
* Trouve les factures d'un abonnement
|
||||
* Trouve toutes les factures d'un abonnement
|
||||
*/
|
||||
List<InvoiceEntity> findBySubscriptionIdOrderByCreatedAtDesc(UUID subscriptionId);
|
||||
|
||||
/**
|
||||
* Trouve les factures d'un abonnement avec pagination
|
||||
*/
|
||||
Page<InvoiceEntity> findBySubscriptionId(UUID subscriptionId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Vérifie l'existence d'une facture par ID Stripe
|
||||
*/
|
||||
boolean existsByStripeInvoiceId(String stripeInvoiceId);
|
||||
|
||||
// ===== RECHERCHES PAR STATUT =====
|
||||
|
||||
/**
|
||||
* Trouve les factures par statut
|
||||
*/
|
||||
List<InvoiceEntity> findByStatusOrderByCreatedAtDesc(InvoiceStatusEntity status);
|
||||
|
||||
/**
|
||||
* Trouve les factures par statut avec pagination
|
||||
* Trouve les factures impayées
|
||||
*/
|
||||
Page<InvoiceEntity> findByStatus(InvoiceStatusEntity status, Pageable pageable);
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.status IN ('OPEN', 'PAYMENT_FAILED') ORDER BY i.dueDate ASC")
|
||||
List<InvoiceEntity> findUnpaidInvoices();
|
||||
|
||||
/**
|
||||
* Trouve les factures ouvertes (en attente de paiement)
|
||||
* Trouve les factures en retard
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.status = 'OPEN' ORDER BY i.dueDate ASC NULLS LAST")
|
||||
List<InvoiceEntity> findOpenInvoices();
|
||||
|
||||
/**
|
||||
* Trouve les factures payées
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.status = 'PAID' ORDER BY i.paidAt DESC")
|
||||
List<InvoiceEntity> findPaidInvoices();
|
||||
|
||||
/**
|
||||
* Trouve les factures échouées
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.status = 'PAYMENT_FAILED' ORDER BY i.createdAt DESC")
|
||||
List<InvoiceEntity> findFailedInvoices();
|
||||
|
||||
// ===== RECHERCHES PAR DATE =====
|
||||
|
||||
/**
|
||||
* Trouve les factures échues
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.dueDate < :now AND i.status = 'OPEN' ORDER BY i.dueDate ASC")
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.status = 'OPEN' AND i.dueDate < :now ORDER BY i.dueDate ASC")
|
||||
List<InvoiceEntity> findOverdueInvoices(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* Trouve les factures échues depuis plus de X jours
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.dueDate < :cutoffDate AND i.status = 'OPEN' ORDER BY i.dueDate ASC")
|
||||
List<InvoiceEntity> findInvoicesOverdueSince(@Param("cutoffDate") LocalDateTime cutoffDate);
|
||||
|
||||
/**
|
||||
* Trouve les factures avec échéance dans les X prochains jours
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.dueDate BETWEEN :now AND :endDate AND i.status = 'OPEN' ORDER BY i.dueDate ASC")
|
||||
List<InvoiceEntity> findInvoicesDueSoon(@Param("now") LocalDateTime now, @Param("endDate") LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* Trouve les factures créées dans une période
|
||||
*/
|
||||
List<InvoiceEntity> findByCreatedAtBetweenOrderByCreatedAtDesc(LocalDateTime startDate, LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* Trouve les factures payées dans une période
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.paidAt BETWEEN :startDate AND :endDate ORDER BY i.paidAt DESC")
|
||||
List<InvoiceEntity> findInvoicesPaidBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||
|
||||
// ===== RECHERCHES PAR PÉRIODE DE FACTURATION =====
|
||||
|
||||
/**
|
||||
* Trouve les factures pour une période de facturation spécifique
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.billingPeriodStart = :periodStart AND i.billingPeriodEnd = :periodEnd")
|
||||
List<InvoiceEntity> findByBillingPeriod(@Param("periodStart") LocalDateTime periodStart, @Param("periodEnd") LocalDateTime periodEnd);
|
||||
|
||||
/**
|
||||
* Trouve les factures qui chevauchent une période donnée
|
||||
*/
|
||||
@Query("""
|
||||
SELECT i FROM InvoiceEntity i
|
||||
WHERE i.billingPeriodStart < :endDate
|
||||
AND i.billingPeriodEnd > :startDate
|
||||
ORDER BY i.billingPeriodStart ASC
|
||||
""")
|
||||
List<InvoiceEntity> findInvoicesOverlappingPeriod(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||
|
||||
// ===== RECHERCHES PAR MONTANT =====
|
||||
|
||||
/**
|
||||
* Trouve les factures par montant minimum
|
||||
*/
|
||||
List<InvoiceEntity> findByAmountDueGreaterThanEqualOrderByAmountDueDesc(BigDecimal minAmount);
|
||||
|
||||
/**
|
||||
* Trouve les factures dans une fourchette de montants
|
||||
*/
|
||||
List<InvoiceEntity> findByAmountDueBetweenOrderByAmountDueDesc(BigDecimal minAmount, BigDecimal maxAmount);
|
||||
|
||||
/**
|
||||
* Trouve les petites factures (moins de X euros)
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.amountDue < :maxAmount ORDER BY i.amountDue ASC")
|
||||
List<InvoiceEntity> findSmallInvoices(@Param("maxAmount") BigDecimal maxAmount);
|
||||
|
||||
/**
|
||||
* Trouve les grosses factures (plus de X euros)
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.amountDue > :minAmount ORDER BY i.amountDue DESC")
|
||||
List<InvoiceEntity> findLargeInvoices(@Param("minAmount") BigDecimal minAmount);
|
||||
|
||||
// ===== RECHERCHES PAR TENTATIVES DE PAIEMENT =====
|
||||
|
||||
/**
|
||||
* Trouve les factures avec plusieurs tentatives d'échec
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.attemptCount > :minAttempts ORDER BY i.attemptCount DESC")
|
||||
List<InvoiceEntity> findInvoicesWithMultipleAttempts(@Param("minAttempts") int minAttempts);
|
||||
|
||||
/**
|
||||
* Trouve les factures nécessitant une attention (échouées plusieurs fois)
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.attemptCount >= 3 AND i.status IN ('OPEN', 'PAYMENT_FAILED') ORDER BY i.attemptCount DESC")
|
||||
List<InvoiceEntity> findInvoicesRequiringAttention();
|
||||
|
||||
// ===== RECHERCHES PAR ABONNEMENT =====
|
||||
|
||||
/**
|
||||
* Trouve la dernière facture d'un abonnement
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.subscription.id = :subscriptionId ORDER BY i.createdAt DESC LIMIT 1")
|
||||
Optional<InvoiceEntity> findLatestInvoiceBySubscription(@Param("subscriptionId") UUID subscriptionId);
|
||||
|
||||
/**
|
||||
* Trouve la première facture d'un abonnement
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.subscription.id = :subscriptionId ORDER BY i.createdAt ASC LIMIT 1")
|
||||
Optional<InvoiceEntity> findFirstInvoiceBySubscription(@Param("subscriptionId") UUID subscriptionId);
|
||||
|
||||
/**
|
||||
* Compte les factures d'un abonnement
|
||||
*/
|
||||
long countBySubscriptionId(UUID subscriptionId);
|
||||
|
||||
/**
|
||||
* Trouve les factures impayées d'un abonnement
|
||||
*/
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.subscription.id = :subscriptionId AND i.status IN ('OPEN', 'PAYMENT_FAILED') ORDER BY i.dueDate ASC")
|
||||
List<InvoiceEntity> findUnpaidInvoicesBySubscription(@Param("subscriptionId") UUID subscriptionId);
|
||||
|
||||
// ===== STATISTIQUES ET MÉTRIQUES =====
|
||||
@Query("SELECT i FROM InvoiceEntity i WHERE i.status = 'PAID' AND i.paidAt BETWEEN :startDate AND :endDate ORDER BY i.paidAt DESC")
|
||||
List<InvoiceEntity> findPaidInvoicesBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* Compte les factures par statut
|
||||
@ -200,133 +57,7 @@ public interface InvoiceJpaRepository extends JpaRepository<InvoiceEntity, UUID>
|
||||
long countByStatus(InvoiceStatusEntity status);
|
||||
|
||||
/**
|
||||
* Calcule le montant total facturé dans une période
|
||||
* Vérifie l'existence d'une facture par ID Stripe
|
||||
*/
|
||||
@Query("SELECT COALESCE(SUM(i.amountDue), 0) FROM InvoiceEntity i WHERE i.createdAt BETWEEN :startDate AND :endDate")
|
||||
BigDecimal calculateTotalInvoicedBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* Calcule le montant total payé dans une période
|
||||
*/
|
||||
@Query("SELECT COALESCE(SUM(i.amountPaid), 0) FROM InvoiceEntity i WHERE i.paidAt BETWEEN :startDate AND :endDate")
|
||||
BigDecimal calculateTotalPaidBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* Calcule le montant total en attente
|
||||
*/
|
||||
@Query("SELECT COALESCE(SUM(i.amountDue - COALESCE(i.amountPaid, 0)), 0) FROM InvoiceEntity i WHERE i.status = 'OPEN'")
|
||||
BigDecimal calculateTotalOutstanding();
|
||||
|
||||
/**
|
||||
* Calcule le montant total en retard
|
||||
*/
|
||||
@Query("SELECT COALESCE(SUM(i.amountDue - COALESCE(i.amountPaid, 0)), 0) FROM InvoiceEntity i WHERE i.status = 'OPEN' AND i.dueDate < :now")
|
||||
BigDecimal calculateTotalOverdue(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* Calcule le taux de paiement (pourcentage de factures payées)
|
||||
*/
|
||||
@Query("""
|
||||
SELECT CAST(
|
||||
COUNT(CASE WHEN i.status = 'PAID' THEN 1 END) * 100.0 /
|
||||
NULLIF(COUNT(i), 0)
|
||||
AS DOUBLE
|
||||
)
|
||||
FROM InvoiceEntity i
|
||||
WHERE i.createdAt BETWEEN :startDate AND :endDate
|
||||
""")
|
||||
Double calculatePaymentRate(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||
|
||||
// ===== RAPPORTS =====
|
||||
|
||||
/**
|
||||
* Rapport des revenus par mois
|
||||
*/
|
||||
@Query("""
|
||||
SELECT
|
||||
YEAR(i.paidAt) as year,
|
||||
MONTH(i.paidAt) as month,
|
||||
COUNT(i) as invoiceCount,
|
||||
SUM(i.amountPaid) as totalRevenue
|
||||
FROM InvoiceEntity i
|
||||
WHERE i.status = 'PAID'
|
||||
AND i.paidAt BETWEEN :startDate AND :endDate
|
||||
GROUP BY YEAR(i.paidAt), MONTH(i.paidAt)
|
||||
ORDER BY year DESC, month DESC
|
||||
""")
|
||||
List<Object[]> getMonthlyRevenueReport(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* Top des abonnements par revenus
|
||||
*/
|
||||
@Query("""
|
||||
SELECT
|
||||
i.subscription.stripeSubscriptionId,
|
||||
COUNT(i) as invoiceCount,
|
||||
SUM(i.amountPaid) as totalRevenue
|
||||
FROM InvoiceEntity i
|
||||
WHERE i.status = 'PAID'
|
||||
GROUP BY i.subscription.id, i.subscription.stripeSubscriptionId
|
||||
ORDER BY totalRevenue DESC
|
||||
""")
|
||||
List<Object[]> getTopSubscriptionsByRevenue(Pageable pageable);
|
||||
|
||||
// ===== OPÉRATIONS DE MAINTENANCE =====
|
||||
|
||||
/**
|
||||
* Met à jour les factures échues
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE InvoiceEntity i SET i.attemptCount = i.attemptCount + 1 WHERE i.dueDate < :now AND i.status = 'OPEN'")
|
||||
int incrementAttemptsForOverdueInvoices(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* Marque les factures anciennes comme irrécouvrables
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE InvoiceEntity i SET i.status = 'UNCOLLECTIBLE' WHERE i.dueDate < :cutoffDate AND i.status = 'OPEN' AND i.attemptCount > :maxAttempts")
|
||||
int markOldInvoicesAsUncollectible(@Param("cutoffDate") LocalDateTime cutoffDate, @Param("maxAttempts") int maxAttempts);
|
||||
|
||||
/**
|
||||
* Archive les anciennes factures payées
|
||||
*/
|
||||
@Modifying
|
||||
@Query("DELETE FROM InvoiceEntity i WHERE i.status = 'PAID' AND i.paidAt < :cutoffDate")
|
||||
int deleteOldPaidInvoices(@Param("cutoffDate") LocalDateTime cutoffDate);
|
||||
|
||||
// ===== RECHERCHES AVANCÉES =====
|
||||
|
||||
/**
|
||||
* Recherche full-text dans les factures
|
||||
*/
|
||||
@Query("""
|
||||
SELECT i FROM InvoiceEntity i
|
||||
WHERE i.invoiceNumber LIKE %:searchTerm%
|
||||
OR i.stripeInvoiceId LIKE %:searchTerm%
|
||||
OR i.subscription.stripeSubscriptionId LIKE %:searchTerm%
|
||||
ORDER BY i.createdAt DESC
|
||||
""")
|
||||
List<InvoiceEntity> searchInvoices(@Param("searchTerm") String searchTerm);
|
||||
|
||||
/**
|
||||
* Trouve les factures par multiple critères
|
||||
*/
|
||||
@Query("""
|
||||
SELECT i FROM InvoiceEntity i
|
||||
WHERE (:status IS NULL OR i.status = :status)
|
||||
AND (:subscriptionId IS NULL OR i.subscription.id = :subscriptionId)
|
||||
AND (:minAmount IS NULL OR i.amountDue >= :minAmount)
|
||||
AND (:maxAmount IS NULL OR i.amountDue <= :maxAmount)
|
||||
AND (:startDate IS NULL OR i.createdAt >= :startDate)
|
||||
AND (:endDate IS NULL OR i.createdAt <= :endDate)
|
||||
ORDER BY i.createdAt DESC
|
||||
""")
|
||||
List<InvoiceEntity> findByMultipleCriteria(
|
||||
@Param("status") InvoiceStatusEntity status,
|
||||
@Param("subscriptionId") UUID subscriptionId,
|
||||
@Param("minAmount") BigDecimal minAmount,
|
||||
@Param("maxAmount") BigDecimal maxAmount,
|
||||
@Param("startDate") LocalDateTime startDate,
|
||||
@Param("endDate") LocalDateTime endDate
|
||||
);
|
||||
}
|
||||
boolean existsByStripeInvoiceId(String stripeInvoiceId);
|
||||
}
|
||||
|
||||
@ -95,17 +95,7 @@ public interface PaymentMethodJpaRepository extends JpaRepository<PaymentMethodE
|
||||
/**
|
||||
* Trouve les cartes expirant bientôt
|
||||
*/
|
||||
@Query("""
|
||||
SELECT pm FROM PaymentMethodEntity pm
|
||||
WHERE pm.type = 'CARD'
|
||||
AND pm.cardExpYear IS NOT NULL
|
||||
AND pm.cardExpMonth IS NOT NULL
|
||||
AND (
|
||||
pm.cardExpYear < :currentYear
|
||||
OR (pm.cardExpYear = :currentYear AND pm.cardExpMonth <= :currentMonth + :monthsAhead)
|
||||
)
|
||||
ORDER BY pm.cardExpYear ASC, pm.cardExpMonth ASC
|
||||
""")
|
||||
@Query("SELECT pm FROM PaymentMethodEntity pm WHERE pm.type = 'CARD' AND pm.cardExpYear IS NOT NULL AND pm.cardExpMonth IS NOT NULL AND (pm.cardExpYear < :currentYear OR (pm.cardExpYear = :currentYear AND pm.cardExpMonth <= :currentMonth + :monthsAhead)) ORDER BY pm.cardExpYear ASC, pm.cardExpMonth ASC")
|
||||
List<PaymentMethodEntity> findCardsExpiringSoon(
|
||||
@Param("currentYear") int currentYear,
|
||||
@Param("currentMonth") int currentMonth,
|
||||
@ -115,17 +105,7 @@ public interface PaymentMethodJpaRepository extends JpaRepository<PaymentMethodE
|
||||
/**
|
||||
* Trouve les cartes expirées
|
||||
*/
|
||||
@Query("""
|
||||
SELECT pm FROM PaymentMethodEntity pm
|
||||
WHERE pm.type = 'CARD'
|
||||
AND pm.cardExpYear IS NOT NULL
|
||||
AND pm.cardExpMonth IS NOT NULL
|
||||
AND (
|
||||
pm.cardExpYear < :currentYear
|
||||
OR (pm.cardExpYear = :currentYear AND pm.cardExpMonth < :currentMonth)
|
||||
)
|
||||
ORDER BY pm.cardExpYear DESC, pm.cardExpMonth DESC
|
||||
""")
|
||||
@Query("SELECT pm FROM PaymentMethodEntity pm WHERE pm.type = 'CARD' AND pm.cardExpYear IS NOT NULL AND pm.cardExpMonth IS NOT NULL AND (pm.cardExpYear < :currentYear OR (pm.cardExpYear = :currentYear AND pm.cardExpMonth < :currentMonth)) ORDER BY pm.cardExpYear DESC, pm.cardExpMonth DESC")
|
||||
List<PaymentMethodEntity> findExpiredCards(
|
||||
@Param("currentYear") int currentYear,
|
||||
@Param("currentMonth") int currentMonth
|
||||
@ -134,18 +114,7 @@ public interface PaymentMethodJpaRepository extends JpaRepository<PaymentMethodE
|
||||
/**
|
||||
* Trouve les cartes d'une entreprise expirant dans les X mois
|
||||
*/
|
||||
@Query("""
|
||||
SELECT pm FROM PaymentMethodEntity pm
|
||||
WHERE pm.companyId = :companyId
|
||||
AND pm.type = 'CARD'
|
||||
AND pm.cardExpYear IS NOT NULL
|
||||
AND pm.cardExpMonth IS NOT NULL
|
||||
AND (
|
||||
pm.cardExpYear < :currentYear
|
||||
OR (pm.cardExpYear = :currentYear AND pm.cardExpMonth <= :currentMonth + :monthsAhead)
|
||||
)
|
||||
ORDER BY pm.cardExpYear ASC, pm.cardExpMonth ASC
|
||||
""")
|
||||
@Query("SELECT pm FROM PaymentMethodEntity pm WHERE pm.companyId = :companyId AND pm.type = 'CARD' AND pm.cardExpYear IS NOT NULL AND pm.cardExpMonth IS NOT NULL AND (pm.cardExpYear < :currentYear OR (pm.cardExpYear = :currentYear AND pm.cardExpMonth <= :currentMonth + :monthsAhead)) ORDER BY pm.cardExpYear ASC, pm.cardExpMonth ASC")
|
||||
List<PaymentMethodEntity> findCompanyCardsExpiringSoon(
|
||||
@Param("companyId") UUID companyId,
|
||||
@Param("currentYear") int currentYear,
|
||||
@ -177,195 +146,44 @@ public interface PaymentMethodJpaRepository extends JpaRepository<PaymentMethodE
|
||||
UUID companyId, String cardBrand, String cardLast4
|
||||
);
|
||||
|
||||
// ===== RECHERCHES PAR BANQUE =====
|
||||
|
||||
/**
|
||||
* Trouve les méthodes de paiement par banque
|
||||
*/
|
||||
List<PaymentMethodEntity> findByBankNameIgnoreCaseOrderByCreatedAtDesc(String bankName);
|
||||
|
||||
/**
|
||||
* Trouve les méthodes d'une entreprise par banque
|
||||
*/
|
||||
List<PaymentMethodEntity> findByCompanyIdAndBankNameIgnoreCaseOrderByCreatedAtDesc(UUID companyId, String bankName);
|
||||
|
||||
// ===== STATISTIQUES =====
|
||||
|
||||
/**
|
||||
* Compte les méthodes de paiement par entreprise
|
||||
* Compte les méthodes de paiement actives par entreprise
|
||||
*/
|
||||
long countByCompanyId(UUID companyId);
|
||||
|
||||
/**
|
||||
* Compte les méthodes de paiement par type
|
||||
* Compte les cartes actives
|
||||
*/
|
||||
long countByType(PaymentMethodTypeEntity type);
|
||||
@Query("SELECT COUNT(pm) FROM PaymentMethodEntity pm WHERE pm.type = 'CARD'")
|
||||
long countActiveCards();
|
||||
|
||||
/**
|
||||
* Compte les cartes par marque
|
||||
* Compte les méthodes SEPA
|
||||
*/
|
||||
long countByCardBrandIgnoreCase(String cardBrand);
|
||||
@Query("SELECT COUNT(pm) FROM PaymentMethodEntity pm WHERE pm.type = 'SEPA_DEBIT'")
|
||||
long countSepaDebitMethods();
|
||||
|
||||
// ===== OPÉRATIONS DE MISE À JOUR =====
|
||||
|
||||
/**
|
||||
* Statistiques par type de méthode de paiement
|
||||
*/
|
||||
@Query("""
|
||||
SELECT pm.type, COUNT(pm) as methodCount
|
||||
FROM PaymentMethodEntity pm
|
||||
GROUP BY pm.type
|
||||
ORDER BY methodCount DESC
|
||||
""")
|
||||
List<Object[]> getPaymentMethodStatistics();
|
||||
|
||||
/**
|
||||
* Statistiques par marque de carte
|
||||
*/
|
||||
@Query("""
|
||||
SELECT pm.cardBrand, COUNT(pm) as cardCount
|
||||
FROM PaymentMethodEntity pm
|
||||
WHERE pm.type = 'CARD' AND pm.cardBrand IS NOT NULL
|
||||
GROUP BY pm.cardBrand
|
||||
ORDER BY cardCount DESC
|
||||
""")
|
||||
List<Object[]> getCardBrandStatistics();
|
||||
|
||||
// ===== RAPPORTS =====
|
||||
|
||||
/**
|
||||
* Rapport des méthodes de paiement par entreprise
|
||||
*/
|
||||
@Query("""
|
||||
SELECT pm.companyId, pm.type, COUNT(pm) as methodCount
|
||||
FROM PaymentMethodEntity pm
|
||||
GROUP BY pm.companyId, pm.type
|
||||
ORDER BY pm.companyId, methodCount DESC
|
||||
""")
|
||||
List<Object[]> getPaymentMethodsByCompanyReport();
|
||||
|
||||
/**
|
||||
* Rapport des expirations de cartes par mois
|
||||
*/
|
||||
@Query("""
|
||||
SELECT pm.cardExpYear, pm.cardExpMonth, COUNT(pm) as expiringCards
|
||||
FROM PaymentMethodEntity pm
|
||||
WHERE pm.type = 'CARD'
|
||||
AND pm.cardExpYear IS NOT NULL
|
||||
AND pm.cardExpMonth IS NOT NULL
|
||||
GROUP BY pm.cardExpYear, pm.cardExpMonth
|
||||
ORDER BY pm.cardExpYear ASC, pm.cardExpMonth ASC
|
||||
""")
|
||||
List<Object[]> getCardExpirationReport();
|
||||
|
||||
// ===== OPÉRATIONS DE MAINTENANCE =====
|
||||
|
||||
/**
|
||||
* Met à jour les méthodes par défaut d'une entreprise (retire le statut de défaut)
|
||||
* Définit une méthode de paiement comme défaut (retire le défaut des autres)
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE PaymentMethodEntity pm SET pm.isDefault = false WHERE pm.companyId = :companyId AND pm.isDefault = true")
|
||||
int removeAllDefaultsForCompany(@Param("companyId") UUID companyId);
|
||||
@Query("UPDATE PaymentMethodEntity pm SET pm.isDefault = false WHERE pm.companyId = :companyId AND pm.id != :paymentMethodId")
|
||||
void unsetOtherDefaultsForCompany(@Param("companyId") UUID companyId, @Param("paymentMethodId") UUID paymentMethodId);
|
||||
|
||||
/**
|
||||
* Définit une méthode comme méthode par défaut (et retire le statut des autres)
|
||||
* Supprime toutes les méthodes de paiement d'une entreprise
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE PaymentMethodEntity pm SET pm.isDefault = CASE WHEN pm.id = :paymentMethodId THEN true ELSE false END WHERE pm.companyId = :companyId")
|
||||
int setAsDefaultPaymentMethod(@Param("companyId") UUID companyId, @Param("paymentMethodId") UUID paymentMethodId);
|
||||
void deleteByCompanyId(UUID companyId);
|
||||
|
||||
/**
|
||||
* Supprime les anciennes méthodes de paiement non utilisées
|
||||
* Supprime les méthodes expirées depuis plus de X jours
|
||||
*/
|
||||
@Modifying
|
||||
@Query("DELETE FROM PaymentMethodEntity pm WHERE pm.createdAt < :cutoffDate AND pm.isDefault = false")
|
||||
int deleteOldUnusedPaymentMethods(@Param("cutoffDate") LocalDateTime cutoffDate);
|
||||
|
||||
// ===== VALIDATION ET VÉRIFICATIONS =====
|
||||
|
||||
/**
|
||||
* Trouve les entreprises avec plusieurs méthodes par défaut (problème de données)
|
||||
*/
|
||||
@Query("""
|
||||
SELECT pm.companyId, COUNT(pm) as defaultCount
|
||||
FROM PaymentMethodEntity pm
|
||||
WHERE pm.isDefault = true
|
||||
GROUP BY pm.companyId
|
||||
HAVING COUNT(pm) > 1
|
||||
""")
|
||||
List<Object[]> findCompaniesWithMultipleDefaults();
|
||||
|
||||
/**
|
||||
* Trouve les méthodes de paiement orphelines (entreprise inexistante)
|
||||
*/
|
||||
@Query("""
|
||||
SELECT pm FROM PaymentMethodEntity pm
|
||||
WHERE pm.companyId NOT IN (SELECT c.id FROM Company c)
|
||||
""")
|
||||
List<PaymentMethodEntity> findOrphanedPaymentMethods();
|
||||
|
||||
/**
|
||||
* Trouve les entreprises sans méthode de paiement
|
||||
*/
|
||||
@Query("""
|
||||
SELECT c.id, c.name FROM Company c
|
||||
WHERE c.id NOT IN (SELECT DISTINCT pm.companyId FROM PaymentMethodEntity pm)
|
||||
""")
|
||||
List<Object[]> findCompaniesWithoutPaymentMethods();
|
||||
|
||||
// ===== RECHERCHES AVANCÉES =====
|
||||
|
||||
/**
|
||||
* Recherche full-text dans les méthodes de paiement
|
||||
*/
|
||||
@Query("""
|
||||
SELECT pm FROM PaymentMethodEntity pm
|
||||
WHERE pm.stripePaymentMethodId LIKE %:searchTerm%
|
||||
OR LOWER(pm.cardBrand) LIKE LOWER(CONCAT('%', :searchTerm, '%'))
|
||||
OR pm.cardLast4 LIKE %:searchTerm%
|
||||
OR LOWER(pm.bankName) LIKE LOWER(CONCAT('%', :searchTerm, '%'))
|
||||
ORDER BY pm.createdAt DESC
|
||||
""")
|
||||
List<PaymentMethodEntity> searchPaymentMethods(@Param("searchTerm") String searchTerm);
|
||||
|
||||
/**
|
||||
* Trouve les méthodes de paiement par multiple critères
|
||||
*/
|
||||
@Query("""
|
||||
SELECT pm FROM PaymentMethodEntity pm
|
||||
WHERE (:companyId IS NULL OR pm.companyId = :companyId)
|
||||
AND (:type IS NULL OR pm.type = :type)
|
||||
AND (:isDefault IS NULL OR pm.isDefault = :isDefault)
|
||||
AND (:cardBrand IS NULL OR LOWER(pm.cardBrand) = LOWER(:cardBrand))
|
||||
ORDER BY pm.isDefault DESC, pm.createdAt DESC
|
||||
""")
|
||||
List<PaymentMethodEntity> findByMultipleCriteria(
|
||||
@Param("companyId") UUID companyId,
|
||||
@Param("type") PaymentMethodTypeEntity type,
|
||||
@Param("isDefault") Boolean isDefault,
|
||||
@Param("cardBrand") String cardBrand
|
||||
);
|
||||
|
||||
/**
|
||||
* Trouve les méthodes de paiement supportant les paiements récurrents
|
||||
*/
|
||||
@Query("SELECT pm FROM PaymentMethodEntity pm WHERE pm.type IN ('CARD', 'SEPA_DEBIT') ORDER BY pm.isDefault DESC, pm.createdAt DESC")
|
||||
List<PaymentMethodEntity> findRecurringPaymentCapableMethods();
|
||||
|
||||
/**
|
||||
* Trouve les méthodes de paiement nécessitant une attention (expirées ou expirant bientôt)
|
||||
*/
|
||||
@Query("""
|
||||
SELECT pm FROM PaymentMethodEntity pm
|
||||
WHERE pm.type = 'CARD'
|
||||
AND pm.cardExpYear IS NOT NULL
|
||||
AND pm.cardExpMonth IS NOT NULL
|
||||
AND (
|
||||
pm.cardExpYear < :currentYear
|
||||
OR (pm.cardExpYear = :currentYear AND pm.cardExpMonth <= :currentMonth + 2)
|
||||
)
|
||||
ORDER BY pm.isDefault DESC, pm.cardExpYear ASC, pm.cardExpMonth ASC
|
||||
""")
|
||||
List<PaymentMethodEntity> findPaymentMethodsRequiringAttention(
|
||||
@Param("currentYear") int currentYear,
|
||||
@Param("currentMonth") int currentMonth
|
||||
);
|
||||
}
|
||||
@Query("DELETE FROM PaymentMethodEntity pm WHERE pm.type = 'CARD' AND pm.cardExpYear < :year OR (pm.cardExpYear = :year AND pm.cardExpMonth < :month)")
|
||||
void deleteExpiredCards(@Param("year") int year, @Param("month") int month);
|
||||
}
|
||||
|
||||
@ -104,26 +104,6 @@ public interface PlanJpaRepository extends JpaRepository<PlanEntity, UUID> {
|
||||
@Query("SELECT p FROM PlanEntity p WHERE p.monthlyPrice > 0 AND p.isActive = true ORDER BY p.monthlyPrice ASC")
|
||||
List<PlanEntity> findPaidPlansOrderByPrice();
|
||||
|
||||
// ===== RECHERCHES PAR FONCTIONNALITÉS =====
|
||||
|
||||
/**
|
||||
* Trouve les plans incluant une fonctionnalité spécifique
|
||||
*/
|
||||
@Query("SELECT p FROM PlanEntity p WHERE JSON_CONTAINS(p.features, :feature, '$') = 1 AND p.isActive = true")
|
||||
List<PlanEntity> findPlansWithFeature(@Param("feature") String feature);
|
||||
|
||||
/**
|
||||
* Trouve les plans avec un nombre spécifique de fonctionnalités
|
||||
*/
|
||||
@Query("SELECT p FROM PlanEntity p WHERE JSON_LENGTH(p.features) = :featureCount AND p.isActive = true")
|
||||
List<PlanEntity> findPlansByFeatureCount(@Param("featureCount") int featureCount);
|
||||
|
||||
/**
|
||||
* Trouve les plans avec un nombre minimum de fonctionnalités
|
||||
*/
|
||||
@Query("SELECT p FROM PlanEntity p WHERE JSON_LENGTH(p.features) >= :minFeatures AND p.isActive = true ORDER BY JSON_LENGTH(p.features) ASC")
|
||||
List<PlanEntity> findPlansWithMinimumFeatures(@Param("minFeatures") int minFeatures);
|
||||
|
||||
// ===== RECHERCHES PAR UTILISATEURS =====
|
||||
|
||||
/**
|
||||
@ -140,8 +120,8 @@ public interface PlanJpaRepository extends JpaRepository<PlanEntity, UUID> {
|
||||
/**
|
||||
* Trouve le plan le plus économique pour un nombre d'utilisateurs donné
|
||||
*/
|
||||
@Query("SELECT p FROM PlanEntity p WHERE (p.maxUsers >= :userCount OR p.maxUsers = -1) AND p.isActive = true ORDER BY p.monthlyPrice ASC LIMIT 1")
|
||||
Optional<PlanEntity> findCheapestPlanForUsers(@Param("userCount") int userCount);
|
||||
@Query("SELECT p FROM PlanEntity p WHERE (p.maxUsers >= :userCount OR p.maxUsers = -1) AND p.isActive = true ORDER BY p.monthlyPrice ASC")
|
||||
List<PlanEntity> findCheapestPlansForUsers(@Param("userCount") int userCount);
|
||||
|
||||
// ===== RECHERCHES PAR PÉRIODE D'ESSAI =====
|
||||
|
||||
@ -158,18 +138,6 @@ public interface PlanJpaRepository extends JpaRepository<PlanEntity, UUID> {
|
||||
|
||||
// ===== RECHERCHES POUR RECOMMANDATIONS =====
|
||||
|
||||
/**
|
||||
* Trouve les plans recommandés (avec métadonnées spécifiques)
|
||||
*/
|
||||
@Query("SELECT p FROM PlanEntity p WHERE JSON_EXTRACT(p.metadata, '$.recommended') = true AND p.isActive = true ORDER BY p.displayOrder ASC")
|
||||
List<PlanEntity> findRecommendedPlans();
|
||||
|
||||
/**
|
||||
* Trouve les plans populaires
|
||||
*/
|
||||
@Query("SELECT p FROM PlanEntity p WHERE JSON_EXTRACT(p.metadata, '$.popular') = true AND p.isActive = true ORDER BY p.displayOrder ASC")
|
||||
List<PlanEntity> findPopularPlans();
|
||||
|
||||
/**
|
||||
* Trouve les plans pour entreprises
|
||||
*/
|
||||
@ -181,37 +149,20 @@ public interface PlanJpaRepository extends JpaRepository<PlanEntity, UUID> {
|
||||
/**
|
||||
* Trouve les plans dans une gamme de prix pour comparaison
|
||||
*/
|
||||
@Query("""
|
||||
SELECT p FROM PlanEntity p
|
||||
WHERE p.isActive = true
|
||||
AND p.monthlyPrice BETWEEN :basePrice * 0.5 AND :basePrice * 2
|
||||
ORDER BY p.monthlyPrice ASC
|
||||
""")
|
||||
List<PlanEntity> findPlansForComparison(@Param("basePrice") BigDecimal basePrice);
|
||||
@Query("SELECT p FROM PlanEntity p WHERE p.isActive = true AND p.monthlyPrice BETWEEN :minPrice AND :maxPrice ORDER BY p.monthlyPrice ASC")
|
||||
List<PlanEntity> findPlansForComparison(@Param("minPrice") BigDecimal minPrice, @Param("maxPrice") BigDecimal maxPrice);
|
||||
|
||||
/**
|
||||
* Trouve le plan supérieur recommandé pour un upgrade
|
||||
*/
|
||||
@Query("""
|
||||
SELECT p FROM PlanEntity p
|
||||
WHERE p.monthlyPrice > :currentPrice
|
||||
AND p.isActive = true
|
||||
ORDER BY p.monthlyPrice ASC
|
||||
LIMIT 1
|
||||
""")
|
||||
Optional<PlanEntity> findNextTierPlan(@Param("currentPrice") BigDecimal currentPrice);
|
||||
@Query("SELECT p FROM PlanEntity p WHERE p.monthlyPrice > :currentPrice AND p.isActive = true ORDER BY p.monthlyPrice ASC")
|
||||
List<PlanEntity> findNextTierPlans(@Param("currentPrice") BigDecimal currentPrice);
|
||||
|
||||
/**
|
||||
* Trouve le plan inférieur pour un downgrade
|
||||
*/
|
||||
@Query("""
|
||||
SELECT p FROM PlanEntity p
|
||||
WHERE p.monthlyPrice < :currentPrice
|
||||
AND p.isActive = true
|
||||
ORDER BY p.monthlyPrice DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
Optional<PlanEntity> findPreviousTierPlan(@Param("currentPrice") BigDecimal currentPrice);
|
||||
@Query("SELECT p FROM PlanEntity p WHERE p.monthlyPrice < :currentPrice AND p.isActive = true ORDER BY p.monthlyPrice DESC")
|
||||
List<PlanEntity> findPreviousTierPlans(@Param("currentPrice") BigDecimal currentPrice);
|
||||
|
||||
// ===== STATISTIQUES =====
|
||||
|
||||
@ -226,60 +177,22 @@ public interface PlanJpaRepository extends JpaRepository<PlanEntity, UUID> {
|
||||
@Query("SELECT AVG(p.monthlyPrice) FROM PlanEntity p WHERE p.monthlyPrice > 0 AND p.isActive = true")
|
||||
Double findAverageMonthlyPrice();
|
||||
|
||||
/**
|
||||
* Trouve le prix mensuel médian des plans actifs
|
||||
*/
|
||||
@Query("""
|
||||
SELECT p.monthlyPrice
|
||||
FROM PlanEntity p
|
||||
WHERE p.monthlyPrice > 0 AND p.isActive = true
|
||||
ORDER BY p.monthlyPrice
|
||||
LIMIT 1 OFFSET (SELECT COUNT(*) FROM PlanEntity WHERE monthlyPrice > 0 AND isActive = true) / 2
|
||||
""")
|
||||
BigDecimal findMedianMonthlyPrice();
|
||||
|
||||
// ===== RECHERCHES AVANCÉES =====
|
||||
|
||||
/**
|
||||
* Recherche full-text dans les plans
|
||||
* Recherche par nom ou type
|
||||
*/
|
||||
@Query("""
|
||||
SELECT p FROM PlanEntity p
|
||||
WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :searchTerm, '%'))
|
||||
OR LOWER(p.type) LIKE LOWER(CONCAT('%', :searchTerm, '%'))
|
||||
OR JSON_SEARCH(p.features, 'one', CONCAT('%', :searchTerm, '%')) IS NOT NULL
|
||||
ORDER BY p.displayOrder ASC
|
||||
""")
|
||||
@Query("SELECT p FROM PlanEntity p WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR LOWER(p.type) LIKE LOWER(CONCAT('%', :searchTerm, '%')) ORDER BY p.displayOrder ASC")
|
||||
List<PlanEntity> searchPlans(@Param("searchTerm") String searchTerm);
|
||||
|
||||
/**
|
||||
* Trouve les plans par multiple critères
|
||||
*/
|
||||
@Query("""
|
||||
SELECT p FROM PlanEntity p
|
||||
WHERE (:isActive IS NULL OR p.isActive = :isActive)
|
||||
AND (:minPrice IS NULL OR p.monthlyPrice >= :minPrice)
|
||||
AND (:maxPrice IS NULL OR p.monthlyPrice <= :maxPrice)
|
||||
AND (:minUsers IS NULL OR p.maxUsers >= :minUsers OR p.maxUsers = -1)
|
||||
ORDER BY p.displayOrder ASC, p.monthlyPrice ASC
|
||||
""")
|
||||
@Query("SELECT p FROM PlanEntity p WHERE (:isActive IS NULL OR p.isActive = :isActive) AND (:minPrice IS NULL OR p.monthlyPrice >= :minPrice) AND (:maxPrice IS NULL OR p.monthlyPrice <= :maxPrice) AND (:minUsers IS NULL OR p.maxUsers >= :minUsers OR p.maxUsers = -1) ORDER BY p.displayOrder ASC, p.monthlyPrice ASC")
|
||||
List<PlanEntity> findByMultipleCriteria(
|
||||
@Param("isActive") Boolean isActive,
|
||||
@Param("minPrice") BigDecimal minPrice,
|
||||
@Param("maxPrice") BigDecimal maxPrice,
|
||||
@Param("minUsers") Integer minUsers
|
||||
);
|
||||
|
||||
/**
|
||||
* Trouve les plans les plus utilisés (basé sur les abonnements)
|
||||
*/
|
||||
@Query("""
|
||||
SELECT p, COUNT(s) as subscriptionCount
|
||||
FROM PlanEntity p
|
||||
LEFT JOIN SubscriptionEntity s ON (s.stripePriceId = p.stripePriceIdMonthly OR s.stripePriceId = p.stripePriceIdYearly)
|
||||
WHERE p.isActive = true
|
||||
GROUP BY p
|
||||
ORDER BY subscriptionCount DESC
|
||||
""")
|
||||
List<Object[]> findMostUsedPlans(Pageable pageable);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,10 +2,7 @@ package com.dh7789dev.xpeditis.repository;
|
||||
|
||||
import com.dh7789dev.xpeditis.entity.SubscriptionEntity;
|
||||
import com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@ -16,117 +13,70 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository JPA pour les abonnements Stripe
|
||||
* Repository JPA pour les abonnements
|
||||
*/
|
||||
@Repository
|
||||
public interface SubscriptionJpaRepository extends JpaRepository<SubscriptionEntity, UUID> {
|
||||
|
||||
// ===== RECHERCHES DE BASE =====
|
||||
|
||||
/**
|
||||
* Trouve un abonnement par son ID Stripe
|
||||
*/
|
||||
Optional<SubscriptionEntity> findByStripeSubscriptionId(String stripeSubscriptionId);
|
||||
|
||||
/**
|
||||
* Trouve un abonnement par son ID client Stripe
|
||||
*/
|
||||
List<SubscriptionEntity> findByStripeCustomerId(String stripeCustomerId);
|
||||
|
||||
/**
|
||||
* Trouve un abonnement par son ID de prix Stripe
|
||||
*/
|
||||
List<SubscriptionEntity> findByStripePriceId(String stripePriceId);
|
||||
Optional<SubscriptionEntity> findByStripePriceId(String stripePriceId);
|
||||
|
||||
/**
|
||||
* Vérifie l'existence d'un abonnement par ID Stripe
|
||||
* Trouve tous les abonnements d'un client Stripe
|
||||
*/
|
||||
boolean existsByStripeSubscriptionId(String stripeSubscriptionId);
|
||||
List<SubscriptionEntity> findByStripeCustomerIdOrderByCreatedAtDesc(String stripeCustomerId);
|
||||
|
||||
// ===== RECHERCHES PAR STATUT =====
|
||||
/**
|
||||
* Trouve l'abonnement actif d'un client
|
||||
*/
|
||||
Optional<SubscriptionEntity> findByStripeCustomerIdAndStatus(String stripeCustomerId, SubscriptionStatusEntity status);
|
||||
|
||||
/**
|
||||
* Trouve les abonnements par statut
|
||||
*/
|
||||
List<SubscriptionEntity> findByStatusOrderByCreatedAtDesc(SubscriptionStatusEntity status);
|
||||
|
||||
/**
|
||||
* Trouve les abonnements par statut avec pagination
|
||||
*/
|
||||
Page<SubscriptionEntity> findByStatus(SubscriptionStatusEntity status, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Trouve les abonnements actifs
|
||||
*/
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'ACTIVE' ORDER BY s.createdAt DESC")
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status IN ('ACTIVE', 'TRIALING') ORDER BY s.currentPeriodEnd ASC")
|
||||
List<SubscriptionEntity> findActiveSubscriptions();
|
||||
|
||||
/**
|
||||
* Trouve les abonnements en période d'essai
|
||||
*/
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'TRIALING' ORDER BY s.trialEndDate ASC")
|
||||
List<SubscriptionEntity> findTrialSubscriptions();
|
||||
List<SubscriptionEntity> findByStatusOrderByTrialEndDateAsc(SubscriptionStatusEntity status);
|
||||
|
||||
/**
|
||||
* Trouve les abonnements nécessitant une attention (problèmes de paiement)
|
||||
* Trouve les essais se terminant bientôt
|
||||
*/
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status IN ('PAST_DUE', 'UNPAID', 'INCOMPLETE') ORDER BY s.nextBillingDate ASC")
|
||||
List<SubscriptionEntity> findSubscriptionsRequiringAttention();
|
||||
|
||||
// ===== RECHERCHES PAR DATE =====
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'TRIALING' AND s.trialEndDate BETWEEN :now AND :futureDate ORDER BY s.trialEndDate ASC")
|
||||
List<SubscriptionEntity> findTrialsEndingSoon(@Param("now") LocalDateTime now, @Param("futureDate") LocalDateTime futureDate);
|
||||
|
||||
/**
|
||||
* Trouve les abonnements dont la période d'essai se termine bientôt
|
||||
* Trouve les abonnements à renouveler bientôt
|
||||
*/
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'TRIALING' AND s.trialEndDate BETWEEN :now AND :endDate ORDER BY s.trialEndDate ASC")
|
||||
List<SubscriptionEntity> findTrialsEndingBetween(@Param("now") LocalDateTime now, @Param("endDate") LocalDateTime endDate);
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'ACTIVE' AND s.currentPeriodEnd BETWEEN :now AND :futureDate ORDER BY s.currentPeriodEnd ASC")
|
||||
List<SubscriptionEntity> findSubscriptionsRenewingSoon(@Param("now") LocalDateTime now, @Param("futureDate") LocalDateTime futureDate);
|
||||
|
||||
/**
|
||||
* Trouve les abonnements avec facturation dans la période spécifiée
|
||||
* Trouve les abonnements en période de grâce
|
||||
*/
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.nextBillingDate BETWEEN :startDate AND :endDate ORDER BY s.nextBillingDate ASC")
|
||||
List<SubscriptionEntity> findSubscriptionsForBillingBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'PAST_DUE' ORDER BY s.currentPeriodEnd ASC")
|
||||
List<SubscriptionEntity> findSubscriptionsInGracePeriod();
|
||||
|
||||
/**
|
||||
* Trouve les abonnements créés dans une période
|
||||
* Trouve les abonnements annulés mais encore actifs
|
||||
*/
|
||||
List<SubscriptionEntity> findByCreatedAtBetweenOrderByCreatedAtDesc(LocalDateTime startDate, LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* Trouve les abonnements expirés (période courante terminée)
|
||||
*/
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.currentPeriodEnd < :now AND s.status NOT IN ('CANCELED', 'UNPAID') ORDER BY s.currentPeriodEnd ASC")
|
||||
List<SubscriptionEntity> findExpiredSubscriptions(@Param("now") LocalDateTime now);
|
||||
|
||||
// ===== RECHERCHES PAR CYCLE DE FACTURATION =====
|
||||
|
||||
/**
|
||||
* Trouve les abonnements mensuels
|
||||
*/
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.billingCycle = 'MONTHLY' ORDER BY s.createdAt DESC")
|
||||
List<SubscriptionEntity> findMonthlySubscriptions();
|
||||
|
||||
/**
|
||||
* Trouve les abonnements annuels
|
||||
*/
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.billingCycle = 'YEARLY' ORDER BY s.createdAt DESC")
|
||||
List<SubscriptionEntity> findYearlySubscriptions();
|
||||
|
||||
// ===== RECHERCHES POUR ANNULATION =====
|
||||
|
||||
/**
|
||||
* Trouve les abonnements marqués pour annulation en fin de période
|
||||
*/
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.cancelAtPeriodEnd = true ORDER BY s.currentPeriodEnd ASC")
|
||||
List<SubscriptionEntity> findSubscriptionsToCancel();
|
||||
|
||||
/**
|
||||
* Trouve les abonnements à annuler dans la période spécifiée
|
||||
*/
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.cancelAtPeriodEnd = true AND s.currentPeriodEnd BETWEEN :startDate AND :endDate ORDER BY s.currentPeriodEnd ASC")
|
||||
List<SubscriptionEntity> findSubscriptionsToCancelBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||
|
||||
// ===== STATISTIQUES ET MÉTRIQUES =====
|
||||
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'CANCELED' AND s.currentPeriodEnd > :now ORDER BY s.currentPeriodEnd ASC")
|
||||
List<SubscriptionEntity> findCanceledButActiveSubscriptions(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* Compte les abonnements par statut
|
||||
@ -134,117 +84,19 @@ public interface SubscriptionJpaRepository extends JpaRepository<SubscriptionEnt
|
||||
long countByStatus(SubscriptionStatusEntity status);
|
||||
|
||||
/**
|
||||
* Compte les abonnements par cycle de facturation
|
||||
* Compte les abonnements actifs
|
||||
*/
|
||||
@Query("SELECT COUNT(s) FROM SubscriptionEntity s WHERE s.billingCycle = :billingCycle")
|
||||
long countByBillingCycle(@Param("billingCycle") String billingCycle);
|
||||
@Query("SELECT COUNT(s) FROM SubscriptionEntity s WHERE s.status IN ('ACTIVE', 'TRIALING')")
|
||||
long countActiveSubscriptions();
|
||||
|
||||
/**
|
||||
* Compte les nouveaux abonnements dans une période
|
||||
* Vérifie l'existence d'un abonnement par ID Stripe
|
||||
*/
|
||||
long countByCreatedAtBetween(LocalDateTime startDate, LocalDateTime endDate);
|
||||
boolean existsByStripeSubscriptionId(String stripeSubscriptionId);
|
||||
|
||||
/**
|
||||
* Calcul du chiffre d'affaires mensuel récurrent (MRR)
|
||||
* Vérifie si un client a un abonnement actif
|
||||
*/
|
||||
@Query("""
|
||||
SELECT SUM(
|
||||
CASE
|
||||
WHEN s.billingCycle = 'MONTHLY' THEN
|
||||
(SELECT p.monthlyPrice FROM PlanEntity p WHERE p.stripePriceIdMonthly = s.stripePriceId
|
||||
OR p.stripePriceIdYearly = s.stripePriceId)
|
||||
WHEN s.billingCycle = 'YEARLY' THEN
|
||||
(SELECT p.yearlyPrice FROM PlanEntity p WHERE p.stripePriceIdMonthly = s.stripePriceId
|
||||
OR p.stripePriceIdYearly = s.stripePriceId) / 12
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
FROM SubscriptionEntity s
|
||||
WHERE s.status = 'ACTIVE'
|
||||
""")
|
||||
Double calculateMonthlyRecurringRevenue();
|
||||
|
||||
/**
|
||||
* Calcul du chiffre d'affaires annuel récurrent (ARR)
|
||||
*/
|
||||
@Query("""
|
||||
SELECT SUM(
|
||||
CASE
|
||||
WHEN s.billingCycle = 'MONTHLY' THEN
|
||||
(SELECT p.monthlyPrice FROM PlanEntity p WHERE p.stripePriceIdMonthly = s.stripePriceId
|
||||
OR p.stripePriceIdYearly = s.stripePriceId) * 12
|
||||
WHEN s.billingCycle = 'YEARLY' THEN
|
||||
(SELECT p.yearlyPrice FROM PlanEntity p WHERE p.stripePriceIdMonthly = s.stripePriceId
|
||||
OR p.stripePriceIdYearly = s.stripePriceId)
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
FROM SubscriptionEntity s
|
||||
WHERE s.status = 'ACTIVE'
|
||||
""")
|
||||
Double calculateAnnualRecurringRevenue();
|
||||
|
||||
// ===== OPÉRATIONS DE MAINTENANCE =====
|
||||
|
||||
/**
|
||||
* Met à jour le statut des abonnements expirés
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE SubscriptionEntity s SET s.status = 'CANCELED' WHERE s.currentPeriodEnd < :now AND s.cancelAtPeriodEnd = true")
|
||||
int cancelExpiredSubscriptions(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* Met à jour le statut des essais expirés
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE SubscriptionEntity s SET s.status = 'INCOMPLETE_EXPIRED' WHERE s.status = 'TRIALING' AND s.trialEndDate < :now")
|
||||
int expireTrialSubscriptions(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* Supprime les anciens abonnements annulés
|
||||
*/
|
||||
@Modifying
|
||||
@Query("DELETE FROM SubscriptionEntity s WHERE s.status = 'CANCELED' AND s.modifiedDate < :cutoffDate")
|
||||
int deleteOldCanceledSubscriptions(@Param("cutoffDate") java.time.Instant cutoffDate);
|
||||
|
||||
// ===== RECHERCHES AVANCÉES =====
|
||||
|
||||
/**
|
||||
* Trouve les abonnements par multiple critères
|
||||
*/
|
||||
@Query("""
|
||||
SELECT s FROM SubscriptionEntity s
|
||||
WHERE (:status IS NULL OR s.status = :status)
|
||||
AND (:billingCycle IS NULL OR s.billingCycle = :billingCycle)
|
||||
AND (:customerId IS NULL OR s.stripeCustomerId = :customerId)
|
||||
ORDER BY s.createdAt DESC
|
||||
""")
|
||||
List<SubscriptionEntity> findByMultipleCriteria(
|
||||
@Param("status") SubscriptionStatusEntity status,
|
||||
@Param("billingCycle") String billingCycle,
|
||||
@Param("customerId") String customerId
|
||||
);
|
||||
|
||||
/**
|
||||
* Recherche full-text dans les abonnements (ID client, subscription ID)
|
||||
*/
|
||||
@Query("""
|
||||
SELECT s FROM SubscriptionEntity s
|
||||
WHERE s.stripeSubscriptionId LIKE %:searchTerm%
|
||||
OR s.stripeCustomerId LIKE %:searchTerm%
|
||||
ORDER BY s.createdAt DESC
|
||||
""")
|
||||
List<SubscriptionEntity> searchSubscriptions(@Param("searchTerm") String searchTerm);
|
||||
|
||||
/**
|
||||
* Trouve les top clients par nombre d'abonnements
|
||||
*/
|
||||
@Query("""
|
||||
SELECT s.stripeCustomerId, COUNT(s) as subscriptionCount
|
||||
FROM SubscriptionEntity s
|
||||
WHERE s.status = 'ACTIVE'
|
||||
GROUP BY s.stripeCustomerId
|
||||
ORDER BY subscriptionCount DESC
|
||||
""")
|
||||
List<Object[]> findTopCustomersBySubscriptionCount(Pageable pageable);
|
||||
}
|
||||
@Query("SELECT COUNT(s) > 0 FROM SubscriptionEntity s WHERE s.stripeCustomerId = :stripeCustomerId AND s.status IN ('ACTIVE', 'TRIALING')")
|
||||
boolean hasActiveSubscription(@Param("stripeCustomerId") String stripeCustomerId);
|
||||
}
|
||||
|
||||
148
test-all-endpoints.sh
Normal file
148
test-all-endpoints.sh
Normal file
@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Couleurs pour l'affichage
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Token JWT
|
||||
TOKEN="eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJ0ZXN0YWRtaW4iLCJpYXQiOjE3NTk0MTgwNjcsImV4cCI6MTc1OTUwNDQ2N30.O3NdNW6W1dcXW4-cqFKi08Wrd_w7_H7DkJf260XvFsEjRUbsO7pXcKXV8aElkEur"
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE} TEST COMPLET DES ENDPOINTS API${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Fonction pour tester un endpoint
|
||||
test_endpoint() {
|
||||
local method=$1
|
||||
local endpoint=$2
|
||||
local auth=$3
|
||||
local data=$4
|
||||
local name=$5
|
||||
|
||||
echo -e "${YELLOW}Testing: ${name}${NC}"
|
||||
echo -e " ${BLUE}${method} ${endpoint}${NC}"
|
||||
|
||||
if [ "$auth" == "true" ]; then
|
||||
if [ -n "$data" ]; then
|
||||
response=$(curl -s -w "\n%{http_code}" -X ${method} \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${data}" \
|
||||
"http://localhost:8080${endpoint}")
|
||||
else
|
||||
response=$(curl -s -w "\n%{http_code}" -X ${method} \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
"http://localhost:8080${endpoint}")
|
||||
fi
|
||||
else
|
||||
if [ -n "$data" ]; then
|
||||
response=$(curl -s -w "\n%{http_code}" -X ${method} \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${data}" \
|
||||
"http://localhost:8080${endpoint}")
|
||||
else
|
||||
response=$(curl -s -w "\n%{http_code}" -X ${method} \
|
||||
"http://localhost:8080${endpoint}")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Extraire le code HTTP (dernière ligne)
|
||||
http_code=$(echo "$response" | tail -n 1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
# Vérifier le code de statut
|
||||
if [[ $http_code -ge 200 && $http_code -lt 300 ]]; then
|
||||
echo -e " ${GREEN}✓ Status: ${http_code}${NC}"
|
||||
if [ -n "$body" ] && [ "$body" != "{}" ] && [ "$body" != "[]" ]; then
|
||||
echo -e " ${GREEN}✓ Response: ${body:0:100}...${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}✓ Response: Empty or minimal${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e " ${RED}✗ Status: ${http_code}${NC}"
|
||||
echo -e " ${RED}✗ Response: ${body:0:200}${NC}"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ===== ENDPOINTS PUBLICS =====
|
||||
echo -e "${BLUE}===== ENDPOINTS PUBLICS =====${NC}"
|
||||
echo ""
|
||||
|
||||
test_endpoint "GET" "/actuator/health" "false" "" "Health Check"
|
||||
|
||||
# ===== AUTHENTICATION =====
|
||||
echo -e "${BLUE}===== AUTHENTICATION =====${NC}"
|
||||
echo ""
|
||||
|
||||
test_endpoint "POST" "/api/v1/auth/login" "false" '{"username":"testadmin","password":"Password123"}' "Login"
|
||||
|
||||
# ===== USER MANAGEMENT =====
|
||||
echo -e "${BLUE}===== USER MANAGEMENT =====${NC}"
|
||||
echo ""
|
||||
|
||||
test_endpoint "GET" "/api/v1/users" "true" "" "Get All Users"
|
||||
test_endpoint "GET" "/api/v1/users/profile" "true" "" "Get User Profile"
|
||||
|
||||
# ===== COMPANY MANAGEMENT =====
|
||||
echo -e "${BLUE}===== COMPANY MANAGEMENT =====${NC}"
|
||||
echo ""
|
||||
|
||||
test_endpoint "GET" "/api/v1/companies" "true" "" "Get All Companies"
|
||||
|
||||
# ===== DOCUMENT MANAGEMENT =====
|
||||
echo -e "${BLUE}===== DOCUMENT MANAGEMENT =====${NC}"
|
||||
echo ""
|
||||
|
||||
test_endpoint "GET" "/api/v1/documents" "true" "" "Get All Documents"
|
||||
|
||||
# ===== EXPORT FOLDER MANAGEMENT =====
|
||||
echo -e "${BLUE}===== EXPORT FOLDER MANAGEMENT =====${NC}"
|
||||
echo ""
|
||||
|
||||
test_endpoint "GET" "/api/v1/export-folders" "true" "" "Get All Export Folders"
|
||||
|
||||
# ===== QUOTE MANAGEMENT =====
|
||||
echo -e "${BLUE}===== QUOTE MANAGEMENT =====${NC}"
|
||||
echo ""
|
||||
|
||||
test_endpoint "GET" "/api/v1/quotes" "true" "" "Get All Quotes"
|
||||
|
||||
# ===== SUBSCRIPTION MANAGEMENT (NEWLY ACTIVATED) =====
|
||||
echo -e "${BLUE}===== SUBSCRIPTION MANAGEMENT (NEWLY ACTIVATED) =====${NC}"
|
||||
echo ""
|
||||
|
||||
test_endpoint "GET" "/api/v1/subscriptions" "true" "" "Get All Subscriptions"
|
||||
|
||||
# ===== PLAN MANAGEMENT (NEWLY ACTIVATED) =====
|
||||
echo -e "${BLUE}===== PLAN MANAGEMENT (NEWLY ACTIVATED) =====${NC}"
|
||||
echo ""
|
||||
|
||||
test_endpoint "GET" "/api/v1/plans" "true" "" "Get All Plans"
|
||||
|
||||
# ===== VESSEL SCHEDULE =====
|
||||
echo -e "${BLUE}===== VESSEL SCHEDULE =====${NC}"
|
||||
echo ""
|
||||
|
||||
test_endpoint "GET" "/api/v1/vessel-schedules" "true" "" "Get All Vessel Schedules"
|
||||
|
||||
# ===== SHIPMENT TRACKING =====
|
||||
echo -e "${BLUE}===== SHIPMENT TRACKING =====${NC}"
|
||||
echo ""
|
||||
|
||||
test_endpoint "GET" "/api/v1/shipments" "true" "" "Get All Shipments"
|
||||
|
||||
# ===== NOTIFICATIONS =====
|
||||
echo -e "${BLUE}===== NOTIFICATIONS =====${NC}"
|
||||
echo ""
|
||||
|
||||
test_endpoint "GET" "/api/v1/notifications" "true" "" "Get All Notifications"
|
||||
|
||||
# ===== RÉSUMÉ =====
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${GREEN} Tests terminés!${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
1
token_response.json
Normal file
1
token_response.json
Normal file
@ -0,0 +1 @@
|
||||
{"access_token":"eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJ0ZXN0YWRtaW4iLCJpYXQiOjE3NTk0MTgwNjcsImV4cCI6MTc1OTUwNDQ2N30.O3NdNW6W1dcXW4-cqFKi08Wrd_w7_H7DkJf260XvFsEjRUbsO7pXcKXV8aElkEur","refresh_token":"eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJ0ZXN0YWRtaW4iLCJpYXQiOjE3NTk0MTgwNjcsImV4cCI6MTc2MDAyMjg2N30.nNt8W56dw2AM65K98nIursmGiRnilqfabTo7be10YqWVqUmbZsF-GDheUV7CMKFK","token_type":"Bearer","expires_in":0,"created_at":"2025-10-02T15:14:27.000+00:00","expires_at":"2025-10-03T15:14:27.000+00:00"}
|
||||
Loading…
Reference in New Issue
Block a user