Compare commits

..

No commits in common. "2158031bbee05a4c0b218647850718f9ac91047d" and "da8da492d2ad027aafdf1834e6d7d8b8b5b232a5" have entirely different histories.

54 changed files with 3985 additions and 3818 deletions

View File

@ -1,49 +0,0 @@
{
"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": []
}
}

View File

@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.RestController;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RestController @RestController
@RequestMapping(value = "/", @RequestMapping(name = "/",
produces = APPLICATION_JSON_VALUE) produces = APPLICATION_JSON_VALUE)
public class IndexRestController { public class IndexRestController {

View File

@ -65,7 +65,7 @@ public class SubscriptionController {
request.getCompanyId(), request.getCompanyId(),
request.getPlanType(), request.getPlanType(),
request.getBillingCycle(), request.getBillingCycle(),
request.getPaymentMethodId() != null ? request.getPaymentMethodId().toString() : null, request.getPaymentMethodId(),
request.getStartTrial(), request.getStartTrial(),
request.getCustomTrialDays() request.getCustomTrialDays()
); );
@ -99,14 +99,14 @@ public class SubscriptionController {
log.debug("Récupération des abonnements - statut: {}, cycle: {}, client: {}", log.debug("Récupération des abonnements - statut: {}, cycle: {}, client: {}",
status, billingCycle, customerId); status, billingCycle, customerId);
List<Subscription> subscriptions = subscriptionService.findSubscriptions( Page<Subscription> subscriptions = subscriptionService.findSubscriptions(
status, billingCycle, customerId, pageable.getPageNumber(), pageable.getPageSize()); status, billingCycle, customerId, pageable);
List<SubscriptionDto.Summary> dtos = subscriptions.stream() List<SubscriptionDto.Summary> dtos = subscriptions.getContent().stream()
.map(dtoMapper::toSummaryDto) .map(dtoMapper::toSummaryDto)
.collect(Collectors.toList()); .collect(Collectors.toList());
Page<SubscriptionDto.Summary> result = new PageImpl<>(dtos, pageable, dtos.size()); Page<SubscriptionDto.Summary> result = new PageImpl<>(dtos, pageable, subscriptions.getTotalElements());
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
} }
@ -167,7 +167,7 @@ public class SubscriptionController {
} }
if (request.getNewPaymentMethodId() != null) { if (request.getNewPaymentMethodId() != null) {
subscriptionService.updatePaymentMethod(subscriptionId, request.getNewPaymentMethodId().toString()); subscriptionService.updatePaymentMethod(subscriptionId, request.getNewPaymentMethodId());
if (updated.isEmpty()) { if (updated.isEmpty()) {
updated = subscriptionService.findById(subscriptionId); updated = subscriptionService.findById(subscriptionId);
} }

View File

@ -22,7 +22,6 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.security.Principal; import java.security.Principal;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@ -122,28 +121,11 @@ public class UserRestController {
@RequestParam(required = false) UUID companyId) { @RequestParam(required = false) UUID companyId) {
log.info("User list request - page: {}, size: {}, companyId: {}", page, size, companyId); log.info("User list request - page: {}, size: {}, companyId: {}", page, size, companyId);
List<UserAccount> userAccounts;
long totalElements;
if (companyId != null) {
userAccounts = userService.findUsersByCompany(companyId, page, size);
totalElements = userService.countUsersByCompany(companyId);
} else {
userAccounts = userService.findAllUsers(page, size);
totalElements = userService.countAllUsers();
}
// Convert to UserResponse DTOs
List<UserResponse> userResponses = userAccounts.stream()
.map(this::mapToUserResponse)
.collect(java.util.stream.Collectors.toList());
// Create Page object
Pageable pageable = PageRequest.of(page, size); Pageable pageable = PageRequest.of(page, size);
Page<UserResponse> users = new org.springframework.data.domain.PageImpl<>(
userResponses, // TODO: Implement pagination and company filtering in service layer
pageable, // For now, return an empty page
totalElements); Page<UserResponse> users = Page.empty(pageable);
return ResponseEntity.ok(users); return ResponseEntity.ok(users);
} }

View File

@ -1,120 +0,0 @@
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);
}
}

View File

@ -1,85 +0,0 @@
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;
}
}

View File

@ -0,0 +1,71 @@
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();
}

View File

@ -6,7 +6,6 @@ import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
import com.dh7789dev.xpeditis.port.in.UserManagementUseCase; import com.dh7789dev.xpeditis.port.in.UserManagementUseCase;
import java.security.Principal; import java.security.Principal;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -33,12 +32,4 @@ public interface UserService extends UserManagementUseCase {
boolean existsByEmail(String email); boolean existsByEmail(String email);
boolean existsByUsername(String username); boolean existsByUsername(String username);
List<UserAccount> findAllUsers(int page, int size);
List<UserAccount> findUsersByCompany(UUID companyId, int page, int size);
long countAllUsers();
long countUsersByCompany(UUID companyId);
} }

View File

@ -1,98 +0,0 @@
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();
}

View File

@ -1,9 +1,7 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
public class AddressServiceImpl implements AddressService{ public class AddressServiceImpl implements AddressService{
} }

View File

@ -21,9 +21,9 @@ import java.time.LocalDateTime;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
@Transactional @Transactional
public class AuthenticationServiceImpl implements AuthenticationService { public class AuthenticationServiceImpl implements AuthenticationService {

View File

@ -19,9 +19,9 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
@Transactional @Transactional
public class CompanyServiceImpl implements CompanyService { public class CompanyServiceImpl implements CompanyService {

View File

@ -11,9 +11,9 @@ import java.time.LocalDate;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class DevisCalculServiceImpl implements DevisCalculService { public class DevisCalculServiceImpl implements DevisCalculService {
private final GrilleTarifaireService grilleTarifaireService; private final GrilleTarifaireService grilleTarifaireService;

View File

@ -1,9 +1,7 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
public class DimensionServiceImpl implements DimensionService { public class DimensionServiceImpl implements DimensionService {
} }

View File

@ -1,9 +1,7 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
public class DocumentServiceImpl implements DocumentService { public class DocumentServiceImpl implements DocumentService {
} }

View File

@ -1,9 +1,7 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
public class ExportFolderServiceImpl implements ExportFolderService { public class ExportFolderServiceImpl implements ExportFolderService {
} }

View File

@ -17,9 +17,9 @@ import java.time.format.DateTimeParseException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class GrilleTarifaireServiceImpl implements GrilleTarifaireService { public class GrilleTarifaireServiceImpl implements GrilleTarifaireService {
private final GrilleTarifaireRepository grilleTarifaireRepository; private final GrilleTarifaireRepository grilleTarifaireRepository;

View File

@ -1,30 +1,18 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.Company; import com.dh7789dev.xpeditis.dto.app.Company;
import lombok.extern.slf4j.Slf4j;
import com.dh7789dev.xpeditis.dto.app.License; import com.dh7789dev.xpeditis.dto.app.License;
import lombok.extern.slf4j.Slf4j;
import com.dh7789dev.xpeditis.dto.app.LicenseType; import com.dh7789dev.xpeditis.dto.app.LicenseType;
import lombok.extern.slf4j.Slf4j;
import com.dh7789dev.xpeditis.dto.app.LicenseStatus; import com.dh7789dev.xpeditis.dto.app.LicenseStatus;
import lombok.extern.slf4j.Slf4j;
import com.dh7789dev.xpeditis.dto.app.PlanFeature; import com.dh7789dev.xpeditis.dto.app.PlanFeature;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDate; import java.time.LocalDate;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import lombok.extern.slf4j.Slf4j;
import java.util.Set; import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import java.util.UUID; import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
public class LicenseServiceImpl implements LicenseService { public class LicenseServiceImpl implements LicenseService {

View File

@ -1,16 +1,11 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import java.text.MessageFormat; import java.text.MessageFormat;
import lombok.extern.slf4j.Slf4j;
import java.util.Locale; import java.util.Locale;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
public class NlsServiceImpl implements NlsService { public class NlsServiceImpl implements NlsService {

View File

@ -1,9 +1,7 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
public class NotificationServiceImpl implements NotificationService { public class NotificationServiceImpl implements NotificationService {
} }

View File

@ -1,37 +1,23 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.Plan; import com.dh7789dev.xpeditis.dto.app.Plan;
import lombok.extern.slf4j.Slf4j;
import com.dh7789dev.xpeditis.dto.app.LicenseType; import com.dh7789dev.xpeditis.dto.app.LicenseType;
import lombok.extern.slf4j.Slf4j;
import com.dh7789dev.xpeditis.dto.app.BillingCycle; import com.dh7789dev.xpeditis.dto.app.BillingCycle;
import lombok.extern.slf4j.Slf4j;
import com.dh7789dev.xpeditis.dto.valueobject.Money; import com.dh7789dev.xpeditis.dto.valueobject.Money;
import lombok.extern.slf4j.Slf4j;
import com.dh7789dev.xpeditis.port.out.PlanRepository; import com.dh7789dev.xpeditis.port.out.PlanRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal; import java.math.BigDecimal;
import lombok.extern.slf4j.Slf4j;
import java.math.RoundingMode; import java.math.RoundingMode;
import lombok.extern.slf4j.Slf4j;
import java.util.List; import java.util.List;
import lombok.extern.slf4j.Slf4j;
import java.util.Set; import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import java.util.UUID; import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList; import java.util.ArrayList;
import lombok.extern.slf4j.Slf4j;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
/** /**
* Implémentation du service de gestion des plans * Implémentation du service de gestion des plans
*/ */
@Slf4j
@Service @Service
public class PlanServiceImpl implements PlanService { public class PlanServiceImpl implements PlanService {
@ -54,11 +40,8 @@ public class PlanServiceImpl implements PlanService {
@Override @Override
public Plan getPlanByType(LicenseType type) { public Plan getPlanByType(LicenseType type) {
List<Plan> plans = planRepository.findByTypeAndIsActiveTrue(type); return planRepository.findByTypeAndIsActiveTrue(type)
if (plans.isEmpty()) { .orElseThrow(() -> new IllegalArgumentException("Plan non trouvé pour le type: " + type));
throw new IllegalArgumentException("Plan non trouvé pour le type: " + type);
}
return plans.get(0);
} }
@Override @Override

View File

@ -1,9 +1,7 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
public class QuoteDetailsServiceImpl implements QuoteDetailsService { public class QuoteDetailsServiceImpl implements QuoteDetailsService {
} }

View File

@ -1,9 +1,7 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
public class QuoteServiceImpl implements QuoteService { public class QuoteServiceImpl implements QuoteService {
} }

View File

@ -1,9 +1,7 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
public class ShipmentTrackingServiceImpl implements ShipmentTrackingService { public class ShipmentTrackingServiceImpl implements ShipmentTrackingService {
} }

View File

@ -1,31 +1,18 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.*; import com.dh7789dev.xpeditis.dto.app.*;
import lombok.extern.slf4j.Slf4j;
import com.dh7789dev.xpeditis.port.out.SubscriptionRepository; import com.dh7789dev.xpeditis.port.out.SubscriptionRepository;
import lombok.extern.slf4j.Slf4j;
import com.dh7789dev.xpeditis.port.out.InvoiceRepository; 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 org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import lombok.extern.slf4j.Slf4j;
import java.util.List; import java.util.List;
import lombok.extern.slf4j.Slf4j;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import java.util.UUID; import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
/** /**
* Implémentation du service de gestion des abonnements * Implémentation du service de gestion des abonnements
*/ */
@Slf4j
@Service @Service
public class SubscriptionServiceImpl implements SubscriptionService { public class SubscriptionServiceImpl implements SubscriptionService {
@ -202,7 +189,7 @@ public class SubscriptionServiceImpl implements SubscriptionService {
} }
@Override @Override
public List<Subscription> findSubscriptionsRequiringAttention() { public List<Subscription> getSubscriptionsRequiringAttention() {
List<Subscription> pastDue = subscriptionRepository.findByStatus(SubscriptionStatus.PAST_DUE); List<Subscription> pastDue = subscriptionRepository.findByStatus(SubscriptionStatus.PAST_DUE);
List<Subscription> unpaid = subscriptionRepository.findByStatus(SubscriptionStatus.UNPAID); List<Subscription> unpaid = subscriptionRepository.findByStatus(SubscriptionStatus.UNPAID);
List<Subscription> incomplete = subscriptionRepository.findByStatus(SubscriptionStatus.INCOMPLETE); List<Subscription> incomplete = subscriptionRepository.findByStatus(SubscriptionStatus.INCOMPLETE);
@ -273,59 +260,4 @@ public class SubscriptionServiceImpl implements SubscriptionService {
} }
} }
} }
// ===== 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);
}
} }

View File

@ -1,22 +1,14 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.UserAccount; import com.dh7789dev.xpeditis.dto.app.UserAccount;
import lombok.extern.slf4j.Slf4j;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest; import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import lombok.extern.slf4j.Slf4j;
import com.dh7789dev.xpeditis.dto.request.RegisterRequest; import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import java.security.Principal; import java.security.Principal;
import lombok.extern.slf4j.Slf4j;
import java.util.Optional; import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import java.util.UUID; import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
public class UserServiceImpl implements UserService { public class UserServiceImpl implements UserService {
@ -93,24 +85,4 @@ public class UserServiceImpl implements UserService {
public boolean existsByUsername(String username) { public boolean existsByUsername(String username) {
return userRepository.existsByUsername(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);
}
} }

View File

@ -1,9 +1,7 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service @Service
public class VesselScheduleServiceImpl implements VesselScheduleService { public class VesselScheduleServiceImpl implements VesselScheduleService {
} }

View File

@ -0,0 +1,277 @@
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;
}
}

View File

@ -31,12 +31,4 @@ public interface UserRepository {
void deactivateUser(UUID id); void deactivateUser(UUID id);
List<UserAccount> findByCompanyIdAndIsActive(UUID companyId, boolean isActive); List<UserAccount> findByCompanyIdAndIsActive(UUID companyId, boolean isActive);
List<UserAccount> findAllUsers(int page, int size);
List<UserAccount> findUsersByCompany(UUID companyId, int page, int size);
long countAllUsers();
long countUsersByCompany(UUID companyId);
} }

View File

@ -1,71 +0,0 @@
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);
}
}

View File

@ -1,99 +0,0 @@
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());
}
}

View File

@ -1,118 +0,0 @@
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();
}
}

View File

@ -231,3 +231,15 @@ public class InvoiceEntity {
incrementAttemptCount(); 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
}

View File

@ -1,13 +0,0 @@
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
}

View File

@ -243,3 +243,18 @@ public class PaymentMethodEntity {
} }
} }
/**
* É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)
}

View File

@ -1,17 +0,0 @@
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)
}

View File

@ -62,8 +62,7 @@ public class SubscriptionEntity {
@Column(name = "trial_end_date") @Column(name = "trial_end_date")
LocalDateTime trialEndDate; LocalDateTime trialEndDate;
@OneToOne(fetch = FetchType.LAZY) @OneToOne(mappedBy = "subscription", cascade = CascadeType.ALL)
@JoinColumn(name = "license_id")
LicenseEntity license; LicenseEntity license;
@OneToMany(mappedBy = "subscription", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @OneToMany(mappedBy = "subscription", cascade = CascadeType.ALL, fetch = FetchType.LAZY)

View File

@ -0,0 +1,134 @@
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;
}
}

View File

@ -1,9 +1,11 @@
package com.dh7789dev.xpeditis.mapper; package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.Invoice; 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.dto.app.InvoiceStatus;
import com.dh7789dev.xpeditis.entity.InvoiceEntity; import com.dh7789dev.xpeditis.entity.InvoiceEntity;
import com.dh7789dev.xpeditis.entity.InvoiceStatusEntity; import com.dh7789dev.xpeditis.entity.InvoiceStatusEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
@ -11,11 +13,19 @@ import java.util.stream.Collectors;
/** /**
* Mapper entre Invoice (domaine) et InvoiceEntity (JPA) * Mapper entre Invoice (domaine) et InvoiceEntity (JPA)
* Version simplifiée sans relations complexes
*/ */
@Component @Component
public class InvoiceMapper { 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 * Convertit une entité JPA en objet domaine
*/ */
@ -28,12 +38,7 @@ public class InvoiceMapper {
invoice.setId(entity.getId()); invoice.setId(entity.getId());
invoice.setStripeInvoiceId(entity.getStripeInvoiceId()); invoice.setStripeInvoiceId(entity.getStripeInvoiceId());
invoice.setInvoiceNumber(entity.getInvoiceNumber()); 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.setAmountDue(entity.getAmountDue());
invoice.setAmountPaid(entity.getAmountPaid()); invoice.setAmountPaid(entity.getAmountPaid());
invoice.setCurrency(entity.getCurrency()); invoice.setCurrency(entity.getCurrency());
@ -46,7 +51,25 @@ public class InvoiceMapper {
invoice.setAttemptCount(entity.getAttemptCount()); invoice.setAttemptCount(entity.getAttemptCount());
invoice.setCreatedAt(entity.getCreatedAt()); invoice.setCreatedAt(entity.getCreatedAt());
// Subscription et LineItems seront chargés séparément si nécessaire // 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);
}
return invoice; return invoice;
} }
@ -63,12 +86,7 @@ public class InvoiceMapper {
entity.setId(domain.getId()); entity.setId(domain.getId());
entity.setStripeInvoiceId(domain.getStripeInvoiceId()); entity.setStripeInvoiceId(domain.getStripeInvoiceId());
entity.setInvoiceNumber(domain.getInvoiceNumber()); 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.setAmountDue(domain.getAmountDue());
entity.setAmountPaid(domain.getAmountPaid()); entity.setAmountPaid(domain.getAmountPaid());
entity.setCurrency(domain.getCurrency()); entity.setCurrency(domain.getCurrency());
@ -81,6 +99,8 @@ public class InvoiceMapper {
entity.setAttemptCount(domain.getAttemptCount()); entity.setAttemptCount(domain.getAttemptCount());
entity.setCreatedAt(domain.getCreatedAt()); entity.setCreatedAt(domain.getCreatedAt());
// Les relations seront définies séparément
return entity; return entity;
} }
@ -92,13 +112,8 @@ public class InvoiceMapper {
return; return;
} }
entity.setStripeInvoiceId(domain.getStripeInvoiceId());
entity.setInvoiceNumber(domain.getInvoiceNumber()); 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.setAmountDue(domain.getAmountDue());
entity.setAmountPaid(domain.getAmountPaid()); entity.setAmountPaid(domain.getAmountPaid());
entity.setCurrency(domain.getCurrency()); entity.setCurrency(domain.getCurrency());
@ -136,4 +151,89 @@ public class InvoiceMapper {
.map(this::toEntity) .map(this::toEntity)
.collect(Collectors.toList()); .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;
};
}
} }

View File

@ -0,0 +1,156 @@
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 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());
}
}

View File

@ -1,7 +1,7 @@
package com.dh7789dev.xpeditis.mapper; package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.PaymentMethod; import com.dh7789dev.xpeditis.dto.app.PaymentMethod;
import com.dh7789dev.xpeditis.dto.app.PaymentType; import com.dh7789dev.xpeditis.dto.app.PaymentMethodType;
import com.dh7789dev.xpeditis.entity.PaymentMethodEntity; import com.dh7789dev.xpeditis.entity.PaymentMethodEntity;
import com.dh7789dev.xpeditis.entity.PaymentMethodTypeEntity; import com.dh7789dev.xpeditis.entity.PaymentMethodTypeEntity;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -11,7 +11,6 @@ import java.util.stream.Collectors;
/** /**
* Mapper entre PaymentMethod (domaine) et PaymentMethodEntity (JPA) * Mapper entre PaymentMethod (domaine) et PaymentMethodEntity (JPA)
* Version simplifiée sans relation Company complexe
*/ */
@Component @Component
public class PaymentMethodMapper { public class PaymentMethodMapper {
@ -27,22 +26,16 @@ public class PaymentMethodMapper {
PaymentMethod paymentMethod = new PaymentMethod(); PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.setId(entity.getId()); paymentMethod.setId(entity.getId());
paymentMethod.setStripePaymentMethodId(entity.getStripePaymentMethodId()); paymentMethod.setStripePaymentMethodId(entity.getStripePaymentMethodId());
paymentMethod.setType(mapTypeToDomain(entity.getType()));
// Conversion enum type paymentMethod.setIsDefault(entity.getIsDefault());
if (entity.getType() != null) {
paymentMethod.setType(PaymentType.valueOf(entity.getType().name()));
}
paymentMethod.setDefault(entity.getIsDefault() != null && entity.getIsDefault());
paymentMethod.setCardBrand(entity.getCardBrand()); paymentMethod.setCardBrand(entity.getCardBrand());
paymentMethod.setCardLast4(entity.getCardLast4()); paymentMethod.setCardLast4(entity.getCardLast4());
paymentMethod.setCardExpMonth(entity.getCardExpMonth()); paymentMethod.setCardExpMonth(entity.getCardExpMonth());
paymentMethod.setCardExpYear(entity.getCardExpYear()); paymentMethod.setCardExpYear(entity.getCardExpYear());
paymentMethod.setBankName(entity.getBankName()); paymentMethod.setBankName(entity.getBankName());
paymentMethod.setCompanyId(entity.getCompanyId());
paymentMethod.setCreatedAt(entity.getCreatedAt()); paymentMethod.setCreatedAt(entity.getCreatedAt());
// Company sera chargée séparément si nécessaire
return paymentMethod; return paymentMethod;
} }
@ -57,22 +50,16 @@ public class PaymentMethodMapper {
PaymentMethodEntity entity = new PaymentMethodEntity(); PaymentMethodEntity entity = new PaymentMethodEntity();
entity.setId(domain.getId()); entity.setId(domain.getId());
entity.setStripePaymentMethodId(domain.getStripePaymentMethodId()); entity.setStripePaymentMethodId(domain.getStripePaymentMethodId());
entity.setType(mapTypeToEntity(domain.getType()));
// Conversion enum type entity.setIsDefault(domain.getIsDefault());
if (domain.getType() != null) {
entity.setType(PaymentMethodTypeEntity.valueOf(domain.getType().name()));
}
entity.setIsDefault(domain.isDefault());
entity.setCardBrand(domain.getCardBrand()); entity.setCardBrand(domain.getCardBrand());
entity.setCardLast4(domain.getCardLast4()); entity.setCardLast4(domain.getCardLast4());
entity.setCardExpMonth(domain.getCardExpMonth()); entity.setCardExpMonth(domain.getCardExpMonth());
entity.setCardExpYear(domain.getCardExpYear()); entity.setCardExpYear(domain.getCardExpYear());
entity.setBankName(domain.getBankName()); entity.setBankName(domain.getBankName());
entity.setCompanyId(domain.getCompanyId());
entity.setCreatedAt(domain.getCreatedAt()); entity.setCreatedAt(domain.getCreatedAt());
// companyId sera défini par le service
return entity; return entity;
} }
@ -84,13 +71,8 @@ public class PaymentMethodMapper {
return; return;
} }
entity.setStripePaymentMethodId(domain.getStripePaymentMethodId()); entity.setType(mapTypeToEntity(domain.getType()));
entity.setIsDefault(domain.getIsDefault());
if (domain.getType() != null) {
entity.setType(PaymentMethodTypeEntity.valueOf(domain.getType().name()));
}
entity.setIsDefault(domain.isDefault());
entity.setCardBrand(domain.getCardBrand()); entity.setCardBrand(domain.getCardBrand());
entity.setCardLast4(domain.getCardLast4()); entity.setCardLast4(domain.getCardLast4());
entity.setCardExpMonth(domain.getCardExpMonth()); entity.setCardExpMonth(domain.getCardExpMonth());
@ -123,4 +105,91 @@ public class PaymentMethodMapper {
.map(this::toEntity) .map(this::toEntity)
.collect(Collectors.toList()); .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;
};
}
} }

View File

@ -1,11 +1,9 @@
package com.dh7789dev.xpeditis.mapper; package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.LicenseType;
import com.dh7789dev.xpeditis.dto.app.Plan; import com.dh7789dev.xpeditis.dto.app.Plan;
import com.dh7789dev.xpeditis.entity.PlanEntity; import com.dh7789dev.xpeditis.entity.PlanEntity;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -28,12 +26,7 @@ public class PlanMapper {
Plan plan = new Plan(); Plan plan = new Plan();
plan.setId(entity.getId()); plan.setId(entity.getId());
plan.setName(entity.getName()); 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.setStripePriceIdMonthly(entity.getStripePriceIdMonthly());
plan.setStripePriceIdYearly(entity.getStripePriceIdYearly()); plan.setStripePriceIdYearly(entity.getStripePriceIdYearly());
plan.setMonthlyPrice(entity.getMonthlyPrice()); plan.setMonthlyPrice(entity.getMonthlyPrice());
@ -41,12 +34,9 @@ public class PlanMapper {
plan.setMaxUsers(entity.getMaxUsers()); plan.setMaxUsers(entity.getMaxUsers());
plan.setFeatures(entity.getFeatures()); plan.setFeatures(entity.getFeatures());
plan.setTrialDurationDays(entity.getTrialDurationDays()); plan.setTrialDurationDays(entity.getTrialDurationDays());
plan.setActive(entity.getIsActive()); plan.setIsActive(entity.getIsActive());
plan.setDisplayOrder(entity.getDisplayOrder()); 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.setCreatedAt(entity.getCreatedAt());
plan.setUpdatedAt(entity.getUpdatedAt()); plan.setUpdatedAt(entity.getUpdatedAt());
@ -64,12 +54,7 @@ public class PlanMapper {
PlanEntity entity = new PlanEntity(); PlanEntity entity = new PlanEntity();
entity.setId(domain.getId()); entity.setId(domain.getId());
entity.setName(domain.getName()); 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.setStripePriceIdMonthly(domain.getStripePriceIdMonthly());
entity.setStripePriceIdYearly(domain.getStripePriceIdYearly()); entity.setStripePriceIdYearly(domain.getStripePriceIdYearly());
entity.setMonthlyPrice(domain.getMonthlyPrice()); entity.setMonthlyPrice(domain.getMonthlyPrice());
@ -77,12 +62,9 @@ public class PlanMapper {
entity.setMaxUsers(domain.getMaxUsers()); entity.setMaxUsers(domain.getMaxUsers());
entity.setFeatures(domain.getFeatures()); entity.setFeatures(domain.getFeatures());
entity.setTrialDurationDays(domain.getTrialDurationDays()); entity.setTrialDurationDays(domain.getTrialDurationDays());
entity.setIsActive(domain.isActive()); entity.setIsActive(domain.getIsActive());
entity.setDisplayOrder(domain.getDisplayOrder()); 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.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt()); entity.setUpdatedAt(domain.getUpdatedAt());
@ -98,11 +80,7 @@ public class PlanMapper {
} }
entity.setName(domain.getName()); entity.setName(domain.getName());
entity.setType(domain.getType());
if (domain.getType() != null) {
entity.setType(domain.getType().name());
}
entity.setStripePriceIdMonthly(domain.getStripePriceIdMonthly()); entity.setStripePriceIdMonthly(domain.getStripePriceIdMonthly());
entity.setStripePriceIdYearly(domain.getStripePriceIdYearly()); entity.setStripePriceIdYearly(domain.getStripePriceIdYearly());
entity.setMonthlyPrice(domain.getMonthlyPrice()); entity.setMonthlyPrice(domain.getMonthlyPrice());
@ -110,9 +88,9 @@ public class PlanMapper {
entity.setMaxUsers(domain.getMaxUsers()); entity.setMaxUsers(domain.getMaxUsers());
entity.setFeatures(domain.getFeatures()); entity.setFeatures(domain.getFeatures());
entity.setTrialDurationDays(domain.getTrialDurationDays()); entity.setTrialDurationDays(domain.getTrialDurationDays());
entity.setIsActive(domain.isActive()); entity.setIsActive(domain.getIsActive());
entity.setDisplayOrder(domain.getDisplayOrder()); entity.setDisplayOrder(domain.getDisplayOrder());
entity.setMetadata(convertMetadataToObjectMap(domain.getMetadata())); entity.setMetadata(domain.getMetadata());
entity.setUpdatedAt(domain.getUpdatedAt()); entity.setUpdatedAt(domain.getUpdatedAt());
} }
@ -153,17 +131,14 @@ public class PlanMapper {
Plan plan = new Plan(); Plan plan = new Plan();
plan.setId(entity.getId()); plan.setId(entity.getId());
plan.setName(entity.getName()); plan.setName(entity.getName());
plan.setType(entity.getType());
if (entity.getType() != null) {
plan.setType(LicenseType.valueOf(entity.getType()));
}
plan.setMonthlyPrice(entity.getMonthlyPrice()); plan.setMonthlyPrice(entity.getMonthlyPrice());
plan.setYearlyPrice(entity.getYearlyPrice()); plan.setYearlyPrice(entity.getYearlyPrice());
plan.setMaxUsers(entity.getMaxUsers()); plan.setMaxUsers(entity.getMaxUsers());
plan.setActive(entity.getIsActive()); plan.setIsActive(entity.getIsActive());
plan.setDisplayOrder(entity.getDisplayOrder()); plan.setDisplayOrder(entity.getDisplayOrder());
// Features simplifiées (juste le count)
if (entity.getFeatures() != null) { if (entity.getFeatures() != null) {
plan.setFeatures(entity.getFeatures()); plan.setFeatures(entity.getFeatures());
} }
@ -233,32 +208,4 @@ public class PlanMapper {
entity.setMetadata(metadata); entity.setMetadata(metadata);
entity.setUpdatedAt(java.time.LocalDateTime.now()); 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);
}
} }

View File

@ -1,11 +1,8 @@
package com.dh7789dev.xpeditis.mapper; package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.BillingCycle; import com.dh7789dev.xpeditis.dto.app.*;
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 com.dh7789dev.xpeditis.entity.SubscriptionEntity;
import com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
@ -13,11 +10,19 @@ import java.util.stream.Collectors;
/** /**
* Mapper entre Subscription (domaine) et SubscriptionEntity (JPA) * Mapper entre Subscription (domaine) et SubscriptionEntity (JPA)
* Version simplifiée sans relations complexes
*/ */
@Component @Component
public class SubscriptionMapper { 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 * Convertit une entité JPA en objet domaine
*/ */
@ -31,27 +36,32 @@ public class SubscriptionMapper {
subscription.setStripeSubscriptionId(entity.getStripeSubscriptionId()); subscription.setStripeSubscriptionId(entity.getStripeSubscriptionId());
subscription.setStripeCustomerId(entity.getStripeCustomerId()); subscription.setStripeCustomerId(entity.getStripeCustomerId());
subscription.setStripePriceId(entity.getStripePriceId()); 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.setCurrentPeriodStart(entity.getCurrentPeriodStart());
subscription.setCurrentPeriodEnd(entity.getCurrentPeriodEnd()); subscription.setCurrentPeriodEnd(entity.getCurrentPeriodEnd());
subscription.setCancelAtPeriodEnd(entity.isCancelAtPeriodEnd()); subscription.setCancelAtPeriodEnd(entity.getCancelAtPeriodEnd());
subscription.setBillingCycle(mapBillingCycleToDomain(entity.getBillingCycle()));
// Conversion enum billing cycle
if (entity.getBillingCycle() != null) {
subscription.setBillingCycle(BillingCycle.valueOf(entity.getBillingCycle().name()));
}
subscription.setNextBillingDate(entity.getNextBillingDate()); subscription.setNextBillingDate(entity.getNextBillingDate());
subscription.setTrialEndDate(entity.getTrialEndDate()); subscription.setTrialEndDate(entity.getTrialEndDate());
subscription.setCreatedAt(entity.getCreatedAt()); subscription.setCreatedAt(entity.getCreatedAt());
subscription.setUpdatedAt(entity.getUpdatedAt()); subscription.setUpdatedAt(entity.getUpdatedAt());
// PaymentMethod, License et Invoices seront chargés séparément si nécessaire // 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);
}
return subscription; return subscription;
} }
@ -69,26 +79,18 @@ public class SubscriptionMapper {
entity.setStripeSubscriptionId(domain.getStripeSubscriptionId()); entity.setStripeSubscriptionId(domain.getStripeSubscriptionId());
entity.setStripeCustomerId(domain.getStripeCustomerId()); entity.setStripeCustomerId(domain.getStripeCustomerId());
entity.setStripePriceId(domain.getStripePriceId()); 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.setCurrentPeriodStart(domain.getCurrentPeriodStart());
entity.setCurrentPeriodEnd(domain.getCurrentPeriodEnd()); entity.setCurrentPeriodEnd(domain.getCurrentPeriodEnd());
entity.setCancelAtPeriodEnd(domain.isCancelAtPeriodEnd()); entity.setCancelAtPeriodEnd(domain.getCancelAtPeriodEnd());
entity.setBillingCycle(mapBillingCycleToEntity(domain.getBillingCycle()));
// Conversion enum billing cycle
if (domain.getBillingCycle() != null) {
entity.setBillingCycle(BillingCycleEntity.valueOf(domain.getBillingCycle().name()));
}
entity.setNextBillingDate(domain.getNextBillingDate()); entity.setNextBillingDate(domain.getNextBillingDate());
entity.setTrialEndDate(domain.getTrialEndDate()); entity.setTrialEndDate(domain.getTrialEndDate());
entity.setCreatedAt(domain.getCreatedAt()); entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt()); entity.setUpdatedAt(domain.getUpdatedAt());
// Relations mappées séparément pour éviter les cycles
return entity; return entity;
} }
@ -103,19 +105,11 @@ public class SubscriptionMapper {
entity.setStripeSubscriptionId(domain.getStripeSubscriptionId()); entity.setStripeSubscriptionId(domain.getStripeSubscriptionId());
entity.setStripeCustomerId(domain.getStripeCustomerId()); entity.setStripeCustomerId(domain.getStripeCustomerId());
entity.setStripePriceId(domain.getStripePriceId()); 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.setCurrentPeriodStart(domain.getCurrentPeriodStart());
entity.setCurrentPeriodEnd(domain.getCurrentPeriodEnd()); entity.setCurrentPeriodEnd(domain.getCurrentPeriodEnd());
entity.setCancelAtPeriodEnd(domain.isCancelAtPeriodEnd()); entity.setCancelAtPeriodEnd(domain.getCancelAtPeriodEnd());
entity.setBillingCycle(mapBillingCycleToEntity(domain.getBillingCycle()));
if (domain.getBillingCycle() != null) {
entity.setBillingCycle(BillingCycleEntity.valueOf(domain.getBillingCycle().name()));
}
entity.setNextBillingDate(domain.getNextBillingDate()); entity.setNextBillingDate(domain.getNextBillingDate());
entity.setTrialEndDate(domain.getTrialEndDate()); entity.setTrialEndDate(domain.getTrialEndDate());
entity.setUpdatedAt(domain.getUpdatedAt()); entity.setUpdatedAt(domain.getUpdatedAt());
@ -146,4 +140,62 @@ public class SubscriptionMapper {
.map(this::toEntity) .map(this::toEntity)
.collect(Collectors.toList()); .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;
};
}
} }

View File

@ -1,61 +0,0 @@
package com.dh7789dev.xpeditis.repository;
import com.dh7789dev.xpeditis.CompanyRepository;
import com.dh7789dev.xpeditis.dao.CompanyDao;
import com.dh7789dev.xpeditis.dto.app.Company;
import com.dh7789dev.xpeditis.entity.CompanyEntity;
import com.dh7789dev.xpeditis.mapper.CompanyMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
@Repository
@RequiredArgsConstructor
public class CompanyJpaRepository implements CompanyRepository {
private final CompanyDao companyDao;
@Override
public Company save(Company company) {
CompanyEntity entity = CompanyMapper.companyToCompanyEntity(company);
CompanyEntity savedEntity = companyDao.save(entity);
log.info("Company saved with ID: {}", savedEntity.getId());
return CompanyMapper.companyEntityToCompany(savedEntity);
}
@Override
public Optional<Company> findById(UUID id) {
return companyDao.findById(id)
.map(CompanyMapper::companyEntityToCompany);
}
@Override
public Optional<Company> findByName(String name) {
return companyDao.findByName(name)
.map(CompanyMapper::companyEntityToCompany);
}
@Override
public List<Company> findAll() {
return companyDao.findAll().stream()
.map(CompanyMapper::companyEntityToCompany)
.collect(Collectors.toList());
}
@Override
public boolean existsByName(String name) {
return companyDao.existsByName(name);
}
@Override
public void deleteById(UUID id) {
companyDao.deleteById(id);
log.info("Company deleted with ID: {}", id);
}
}

View File

@ -1,73 +0,0 @@
package com.dh7789dev.xpeditis.repository;
import com.dh7789dev.xpeditis.OAuth2Provider;
import com.dh7789dev.xpeditis.dto.app.GoogleUserInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Optional;
@Slf4j
@Service
public class GoogleOAuth2Provider implements OAuth2Provider {
@Value("${application.oauth2.google.user-info-uri:https://www.googleapis.com/oauth2/v2/userinfo}")
private String userInfoUri;
private final RestTemplate restTemplate;
public GoogleOAuth2Provider() {
this.restTemplate = new RestTemplate();
}
@Override
public boolean validateToken(String accessToken) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<GoogleUserInfo> response = restTemplate.exchange(
userInfoUri,
HttpMethod.GET,
entity,
GoogleUserInfo.class
);
return response.getStatusCode() == HttpStatus.OK;
} catch (Exception e) {
log.error("Error validating Google token", e);
return false;
}
}
@Override
public Optional<GoogleUserInfo> getUserInfo(String accessToken) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<GoogleUserInfo> response = restTemplate.exchange(
userInfoUri,
HttpMethod.GET,
entity,
GoogleUserInfo.class
);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
log.info("Successfully retrieved Google user info for email: {}", response.getBody().getEmail());
return Optional.of(response.getBody());
}
log.warn("Failed to retrieve Google user info: status={}", response.getStatusCode());
return Optional.empty();
} catch (Exception e) {
log.error("Error retrieving Google user info", e);
return Optional.empty();
}
}
}

View File

@ -2,11 +2,15 @@ package com.dh7789dev.xpeditis.repository;
import com.dh7789dev.xpeditis.entity.InvoiceEntity; import com.dh7789dev.xpeditis.entity.InvoiceEntity;
import com.dh7789dev.xpeditis.entity.InvoiceStatusEntity; 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.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -18,38 +22,177 @@ import java.util.UUID;
@Repository @Repository
public interface InvoiceJpaRepository extends JpaRepository<InvoiceEntity, UUID> { public interface InvoiceJpaRepository extends JpaRepository<InvoiceEntity, UUID> {
// ===== RECHERCHES DE BASE =====
/** /**
* Trouve une facture par son ID Stripe * Trouve une facture par son ID Stripe
*/ */
Optional<InvoiceEntity> findByStripeInvoiceId(String stripeInvoiceId); Optional<InvoiceEntity> findByStripeInvoiceId(String stripeInvoiceId);
/** /**
* Trouve toutes les factures d'un abonnement * Trouve une facture par son numéro
*/
Optional<InvoiceEntity> findByInvoiceNumber(String invoiceNumber);
/**
* Trouve les factures d'un abonnement
*/ */
List<InvoiceEntity> findBySubscriptionIdOrderByCreatedAtDesc(UUID subscriptionId); 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 * Trouve les factures par statut
*/ */
List<InvoiceEntity> findByStatusOrderByCreatedAtDesc(InvoiceStatusEntity status); List<InvoiceEntity> findByStatusOrderByCreatedAtDesc(InvoiceStatusEntity status);
/** /**
* Trouve les factures impayées * Trouve les factures par statut avec pagination
*/ */
@Query("SELECT i FROM InvoiceEntity i WHERE i.status IN ('OPEN', 'PAYMENT_FAILED') ORDER BY i.dueDate ASC") Page<InvoiceEntity> findByStatus(InvoiceStatusEntity status, Pageable pageable);
List<InvoiceEntity> findUnpaidInvoices();
/** /**
* Trouve les factures en retard * Trouve les factures ouvertes (en attente de paiement)
*/ */
@Query("SELECT i FROM InvoiceEntity i WHERE i.status = 'OPEN' AND i.dueDate < :now ORDER BY i.dueDate ASC") @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")
List<InvoiceEntity> findOverdueInvoices(@Param("now") LocalDateTime now); 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 * Trouve les factures payées dans une période
*/ */
@Query("SELECT i FROM InvoiceEntity i WHERE i.status = 'PAID' AND i.paidAt BETWEEN :startDate AND :endDate ORDER BY i.paidAt DESC") @Query("SELECT i FROM InvoiceEntity i WHERE i.paidAt BETWEEN :startDate AND :endDate ORDER BY i.paidAt DESC")
List<InvoiceEntity> findPaidInvoicesBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); 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 =====
/** /**
* Compte les factures par statut * Compte les factures par statut
@ -57,7 +200,133 @@ public interface InvoiceJpaRepository extends JpaRepository<InvoiceEntity, UUID>
long countByStatus(InvoiceStatusEntity status); long countByStatus(InvoiceStatusEntity status);
/** /**
* Vérifie l'existence d'une facture par ID Stripe * Calcule le montant total facturé dans une période
*/ */
boolean existsByStripeInvoiceId(String stripeInvoiceId); @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
);
} }

View File

@ -1,71 +0,0 @@
package com.dh7789dev.xpeditis.repository;
import com.dh7789dev.xpeditis.LicenseRepository;
import com.dh7789dev.xpeditis.dao.LicenseDao;
import com.dh7789dev.xpeditis.dto.app.License;
import com.dh7789dev.xpeditis.entity.LicenseEntity;
import com.dh7789dev.xpeditis.mapper.LicenseMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
@Repository
@RequiredArgsConstructor
public class LicenseJpaRepository implements LicenseRepository {
private final LicenseDao licenseDao;
@Override
public License save(License license) {
LicenseEntity entity = LicenseMapper.licenseToLicenseEntity(license);
LicenseEntity savedEntity = licenseDao.save(entity);
log.info("License saved with ID: {}", savedEntity.getId());
return LicenseMapper.licenseEntityToLicense(savedEntity);
}
@Override
public Optional<License> findById(UUID id) {
return licenseDao.findById(id)
.map(LicenseMapper::licenseEntityToLicense);
}
@Override
public Optional<License> findActiveLicenseByCompanyId(UUID companyId) {
return licenseDao.findActiveLicenseByCompanyId(companyId)
.map(LicenseMapper::licenseEntityToLicense);
}
@Override
public List<License> findByCompanyId(UUID companyId) {
return licenseDao.findByCompanyId(companyId).stream()
.map(LicenseMapper::licenseEntityToLicense)
.collect(Collectors.toList());
}
@Override
public Optional<License> findByLicenseKey(String licenseKey) {
return licenseDao.findByLicenseKey(licenseKey)
.map(LicenseMapper::licenseEntityToLicense);
}
@Override
public void deleteById(UUID id) {
licenseDao.deleteById(id);
log.info("License deleted with ID: {}", id);
}
@Override
public void deactivateLicense(UUID id) {
licenseDao.findById(id).ifPresent(license -> {
license.setActive(false);
licenseDao.save(license);
log.info("License deactivated with ID: {}", id);
});
}
}

View File

@ -95,7 +95,17 @@ public interface PaymentMethodJpaRepository extends JpaRepository<PaymentMethodE
/** /**
* Trouve les cartes expirant bientôt * 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( List<PaymentMethodEntity> findCardsExpiringSoon(
@Param("currentYear") int currentYear, @Param("currentYear") int currentYear,
@Param("currentMonth") int currentMonth, @Param("currentMonth") int currentMonth,
@ -105,7 +115,17 @@ public interface PaymentMethodJpaRepository extends JpaRepository<PaymentMethodE
/** /**
* Trouve les cartes expirées * 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( List<PaymentMethodEntity> findExpiredCards(
@Param("currentYear") int currentYear, @Param("currentYear") int currentYear,
@Param("currentMonth") int currentMonth @Param("currentMonth") int currentMonth
@ -114,7 +134,18 @@ public interface PaymentMethodJpaRepository extends JpaRepository<PaymentMethodE
/** /**
* Trouve les cartes d'une entreprise expirant dans les X mois * 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( List<PaymentMethodEntity> findCompanyCardsExpiringSoon(
@Param("companyId") UUID companyId, @Param("companyId") UUID companyId,
@Param("currentYear") int currentYear, @Param("currentYear") int currentYear,
@ -146,44 +177,195 @@ public interface PaymentMethodJpaRepository extends JpaRepository<PaymentMethodE
UUID companyId, String cardBrand, String cardLast4 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 ===== // ===== STATISTIQUES =====
/** /**
* Compte les méthodes de paiement actives par entreprise * Compte les méthodes de paiement par entreprise
*/ */
long countByCompanyId(UUID companyId); long countByCompanyId(UUID companyId);
/** /**
* Compte les cartes actives * Compte les méthodes de paiement par type
*/ */
@Query("SELECT COUNT(pm) FROM PaymentMethodEntity pm WHERE pm.type = 'CARD'") long countByType(PaymentMethodTypeEntity type);
long countActiveCards();
/** /**
* Compte les méthodes SEPA * Compte les cartes par marque
*/ */
@Query("SELECT COUNT(pm) FROM PaymentMethodEntity pm WHERE pm.type = 'SEPA_DEBIT'") long countByCardBrandIgnoreCase(String cardBrand);
long countSepaDebitMethods();
// ===== OPÉRATIONS DE MISE À JOUR =====
/** /**
* Définit une méthode de paiement comme défaut (retire le défaut des autres) * 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)
*/ */
@Modifying @Modifying
@Query("UPDATE PaymentMethodEntity pm SET pm.isDefault = false WHERE pm.companyId = :companyId AND pm.id != :paymentMethodId") @Query("UPDATE PaymentMethodEntity pm SET pm.isDefault = false WHERE pm.companyId = :companyId AND pm.isDefault = true")
void unsetOtherDefaultsForCompany(@Param("companyId") UUID companyId, @Param("paymentMethodId") UUID paymentMethodId); int removeAllDefaultsForCompany(@Param("companyId") UUID companyId);
/** /**
* Supprime toutes les méthodes de paiement d'une entreprise * Définit une méthode comme méthode par défaut (et retire le statut des autres)
*/ */
@Modifying @Modifying
void deleteByCompanyId(UUID companyId); @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);
/** /**
* Supprime les méthodes expirées depuis plus de X jours * Supprime les anciennes méthodes de paiement non utilisées
*/ */
@Modifying @Modifying
@Query("DELETE FROM PaymentMethodEntity pm WHERE pm.type = 'CARD' AND pm.cardExpYear < :year OR (pm.cardExpYear = :year AND pm.cardExpMonth < :month)") @Query("DELETE FROM PaymentMethodEntity pm WHERE pm.createdAt < :cutoffDate AND pm.isDefault = false")
void deleteExpiredCards(@Param("year") int year, @Param("month") int month); 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
);
} }

View File

@ -104,6 +104,26 @@ 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") @Query("SELECT p FROM PlanEntity p WHERE p.monthlyPrice > 0 AND p.isActive = true ORDER BY p.monthlyPrice ASC")
List<PlanEntity> findPaidPlansOrderByPrice(); 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 ===== // ===== RECHERCHES PAR UTILISATEURS =====
/** /**
@ -120,8 +140,8 @@ public interface PlanJpaRepository extends JpaRepository<PlanEntity, UUID> {
/** /**
* Trouve le plan le plus économique pour un nombre d'utilisateurs donné * 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") @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")
List<PlanEntity> findCheapestPlansForUsers(@Param("userCount") int userCount); Optional<PlanEntity> findCheapestPlanForUsers(@Param("userCount") int userCount);
// ===== RECHERCHES PAR PÉRIODE D'ESSAI ===== // ===== RECHERCHES PAR PÉRIODE D'ESSAI =====
@ -138,6 +158,18 @@ public interface PlanJpaRepository extends JpaRepository<PlanEntity, UUID> {
// ===== RECHERCHES POUR RECOMMANDATIONS ===== // ===== 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 * Trouve les plans pour entreprises
*/ */
@ -149,20 +181,37 @@ public interface PlanJpaRepository extends JpaRepository<PlanEntity, UUID> {
/** /**
* Trouve les plans dans une gamme de prix pour comparaison * Trouve les plans dans une gamme de prix pour comparaison
*/ */
@Query("SELECT p FROM PlanEntity p WHERE p.isActive = true AND p.monthlyPrice BETWEEN :minPrice AND :maxPrice ORDER BY p.monthlyPrice ASC") @Query("""
List<PlanEntity> findPlansForComparison(@Param("minPrice") BigDecimal minPrice, @Param("maxPrice") BigDecimal maxPrice); 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);
/** /**
* Trouve le plan supérieur recommandé pour un upgrade * 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") @Query("""
List<PlanEntity> findNextTierPlans(@Param("currentPrice") BigDecimal currentPrice); 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);
/** /**
* Trouve le plan inférieur pour un downgrade * 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") @Query("""
List<PlanEntity> findPreviousTierPlans(@Param("currentPrice") BigDecimal currentPrice); 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);
// ===== STATISTIQUES ===== // ===== STATISTIQUES =====
@ -177,22 +226,60 @@ public interface PlanJpaRepository extends JpaRepository<PlanEntity, UUID> {
@Query("SELECT AVG(p.monthlyPrice) FROM PlanEntity p WHERE p.monthlyPrice > 0 AND p.isActive = true") @Query("SELECT AVG(p.monthlyPrice) FROM PlanEntity p WHERE p.monthlyPrice > 0 AND p.isActive = true")
Double findAverageMonthlyPrice(); 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 ===== // ===== RECHERCHES AVANCÉES =====
/** /**
* Recherche par nom ou type * Recherche full-text dans les plans
*/ */
@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") @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
""")
List<PlanEntity> searchPlans(@Param("searchTerm") String searchTerm); List<PlanEntity> searchPlans(@Param("searchTerm") String searchTerm);
/** /**
* Trouve les plans par multiple critères * 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( List<PlanEntity> findByMultipleCriteria(
@Param("isActive") Boolean isActive, @Param("isActive") Boolean isActive,
@Param("minPrice") BigDecimal minPrice, @Param("minPrice") BigDecimal minPrice,
@Param("maxPrice") BigDecimal maxPrice, @Param("maxPrice") BigDecimal maxPrice,
@Param("minUsers") Integer minUsers @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);
} }

View File

@ -2,7 +2,10 @@ package com.dh7789dev.xpeditis.repository;
import com.dh7789dev.xpeditis.entity.SubscriptionEntity; import com.dh7789dev.xpeditis.entity.SubscriptionEntity;
import com.dh7789dev.xpeditis.entity.SubscriptionStatusEntity; 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.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@ -13,70 +16,117 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
/** /**
* Repository JPA pour les abonnements * Repository JPA pour les abonnements Stripe
*/ */
@Repository @Repository
public interface SubscriptionJpaRepository extends JpaRepository<SubscriptionEntity, UUID> { public interface SubscriptionJpaRepository extends JpaRepository<SubscriptionEntity, UUID> {
// ===== RECHERCHES DE BASE =====
/** /**
* Trouve un abonnement par son ID Stripe * Trouve un abonnement par son ID Stripe
*/ */
Optional<SubscriptionEntity> findByStripeSubscriptionId(String stripeSubscriptionId); 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 * Trouve un abonnement par son ID de prix Stripe
*/ */
Optional<SubscriptionEntity> findByStripePriceId(String stripePriceId); List<SubscriptionEntity> findByStripePriceId(String stripePriceId);
/** /**
* Trouve tous les abonnements d'un client Stripe * Vérifie l'existence d'un abonnement par ID Stripe
*/ */
List<SubscriptionEntity> findByStripeCustomerIdOrderByCreatedAtDesc(String stripeCustomerId); boolean existsByStripeSubscriptionId(String stripeSubscriptionId);
/** // ===== RECHERCHES PAR STATUT =====
* Trouve l'abonnement actif d'un client
*/
Optional<SubscriptionEntity> findByStripeCustomerIdAndStatus(String stripeCustomerId, SubscriptionStatusEntity status);
/** /**
* Trouve les abonnements par statut * Trouve les abonnements par statut
*/ */
List<SubscriptionEntity> findByStatusOrderByCreatedAtDesc(SubscriptionStatusEntity status); List<SubscriptionEntity> findByStatusOrderByCreatedAtDesc(SubscriptionStatusEntity status);
/**
* Trouve les abonnements par statut avec pagination
*/
Page<SubscriptionEntity> findByStatus(SubscriptionStatusEntity status, Pageable pageable);
/** /**
* Trouve les abonnements actifs * Trouve les abonnements actifs
*/ */
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status IN ('ACTIVE', 'TRIALING') ORDER BY s.currentPeriodEnd ASC") @Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'ACTIVE' ORDER BY s.createdAt DESC")
List<SubscriptionEntity> findActiveSubscriptions(); List<SubscriptionEntity> findActiveSubscriptions();
/** /**
* Trouve les abonnements en période d'essai * Trouve les abonnements en période d'essai
*/ */
List<SubscriptionEntity> findByStatusOrderByTrialEndDateAsc(SubscriptionStatusEntity status); @Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'TRIALING' ORDER BY s.trialEndDate ASC")
List<SubscriptionEntity> findTrialSubscriptions();
/** /**
* Trouve les essais se terminant bientôt * Trouve les abonnements nécessitant une attention (problèmes de paiement)
*/ */
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'TRIALING' AND s.trialEndDate BETWEEN :now AND :futureDate ORDER BY s.trialEndDate ASC") @Query("SELECT s FROM SubscriptionEntity s WHERE s.status IN ('PAST_DUE', 'UNPAID', 'INCOMPLETE') ORDER BY s.nextBillingDate ASC")
List<SubscriptionEntity> findTrialsEndingSoon(@Param("now") LocalDateTime now, @Param("futureDate") LocalDateTime futureDate); List<SubscriptionEntity> findSubscriptionsRequiringAttention();
// ===== RECHERCHES PAR DATE =====
/** /**
* Trouve les abonnements à renouveler bientôt * Trouve les abonnements dont la période d'essai se termine bientôt
*/ */
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'ACTIVE' AND s.currentPeriodEnd BETWEEN :now AND :futureDate ORDER BY s.currentPeriodEnd ASC") @Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'TRIALING' AND s.trialEndDate BETWEEN :now AND :endDate ORDER BY s.trialEndDate ASC")
List<SubscriptionEntity> findSubscriptionsRenewingSoon(@Param("now") LocalDateTime now, @Param("futureDate") LocalDateTime futureDate); List<SubscriptionEntity> findTrialsEndingBetween(@Param("now") LocalDateTime now, @Param("endDate") LocalDateTime endDate);
/** /**
* Trouve les abonnements en période de grâce * Trouve les abonnements avec facturation dans la période spécifiée
*/ */
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'PAST_DUE' ORDER BY s.currentPeriodEnd ASC") @Query("SELECT s FROM SubscriptionEntity s WHERE s.nextBillingDate BETWEEN :startDate AND :endDate ORDER BY s.nextBillingDate ASC")
List<SubscriptionEntity> findSubscriptionsInGracePeriod(); List<SubscriptionEntity> findSubscriptionsForBillingBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
/** /**
* Trouve les abonnements annulés mais encore actifs * Trouve les abonnements créés dans une période
*/ */
@Query("SELECT s FROM SubscriptionEntity s WHERE s.status = 'CANCELED' AND s.currentPeriodEnd > :now ORDER BY s.currentPeriodEnd ASC") List<SubscriptionEntity> findByCreatedAtBetweenOrderByCreatedAtDesc(LocalDateTime startDate, LocalDateTime endDate);
List<SubscriptionEntity> findCanceledButActiveSubscriptions(@Param("now") LocalDateTime now);
/**
* 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 =====
/** /**
* Compte les abonnements par statut * Compte les abonnements par statut
@ -84,19 +134,117 @@ public interface SubscriptionJpaRepository extends JpaRepository<SubscriptionEnt
long countByStatus(SubscriptionStatusEntity status); long countByStatus(SubscriptionStatusEntity status);
/** /**
* Compte les abonnements actifs * Compte les abonnements par cycle de facturation
*/ */
@Query("SELECT COUNT(s) FROM SubscriptionEntity s WHERE s.status IN ('ACTIVE', 'TRIALING')") @Query("SELECT COUNT(s) FROM SubscriptionEntity s WHERE s.billingCycle = :billingCycle")
long countActiveSubscriptions(); long countByBillingCycle(@Param("billingCycle") String billingCycle);
/** /**
* Vérifie l'existence d'un abonnement par ID Stripe * Compte les nouveaux abonnements dans une période
*/ */
boolean existsByStripeSubscriptionId(String stripeSubscriptionId); long countByCreatedAtBetween(LocalDateTime startDate, LocalDateTime endDate);
/** /**
* Vérifie si un client a un abonnement actif * Calcul du chiffre d'affaires mensuel récurrent (MRR)
*/ */
@Query("SELECT COUNT(s) > 0 FROM SubscriptionEntity s WHERE s.stripeCustomerId = :stripeCustomerId AND s.status IN ('ACTIVE', 'TRIALING')") @Query("""
boolean hasActiveSubscription(@Param("stripeCustomerId") String stripeCustomerId); 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);
} }

View File

@ -1,144 +0,0 @@
package com.dh7789dev.xpeditis.repository;
import com.dh7789dev.xpeditis.UserRepository;
import com.dh7789dev.xpeditis.dao.UserDao;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import com.dh7789dev.xpeditis.entity.UserEntity;
import com.dh7789dev.xpeditis.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Repository;
import java.security.Principal;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
@Repository
@RequiredArgsConstructor
public class UserJpaRepository implements UserRepository {
private final UserDao userDao;
private final PasswordEncoder passwordEncoder;
@Override
public void changePassword(ChangePasswordRequest request, Principal connectedUser) {
String username = connectedUser.getName();
UserEntity user = userDao.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
// Verify current password
if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPassword())) {
throw new RuntimeException("Current password is incorrect");
}
// Verify new password matches confirmation
if (!request.getNewPassword().equals(request.getConfirmationPassword())) {
throw new RuntimeException("New password and confirmation do not match");
}
// Update password
user.setPassword(passwordEncoder.encode(request.getNewPassword()));
userDao.save(user);
log.info("Password changed successfully for user: {}", username);
}
@Override
public UserAccount save(UserAccount userAccount) {
UserEntity entity = UserMapper.userAccountToUserEntity(userAccount);
UserEntity savedEntity = userDao.save(entity);
return UserMapper.userEntityToUserAccount(savedEntity);
}
@Override
public Optional<UserAccount> findById(UUID id) {
return userDao.findById(id).map(UserMapper::userEntityToUserAccount);
}
@Override
public Optional<UserAccount> findByEmail(String email) {
return userDao.findByEmail(email).map(UserMapper::userEntityToUserAccount);
}
@Override
public Optional<UserAccount> findByUsername(String username) {
return userDao.findByUsername(username).map(UserMapper::userEntityToUserAccount);
}
@Override
public Optional<UserAccount> findByGoogleId(String googleId) {
return userDao.findByGoogleId(googleId).map(UserMapper::userEntityToUserAccount);
}
@Override
public boolean existsByEmail(String email) {
return userDao.existsByEmail(email);
}
@Override
public boolean existsByUsername(String username) {
return userDao.existsByUsername(username);
}
@Override
public void deleteById(UUID id) {
userDao.deleteById(id);
log.info("User deleted with ID: {}", id);
}
@Override
public void deactivateUser(UUID id) {
UserEntity user = userDao.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
user.setEnabled(false);
userDao.save(user);
log.info("User deactivated with ID: {}", id);
}
@Override
public List<UserAccount> findByCompanyIdAndIsActive(UUID companyId, boolean isActive) {
// This needs to be implemented in UserDao
log.warn("findByCompanyIdAndIsActive not yet implemented in UserDao");
return List.of();
}
@Override
public List<UserAccount> findAllUsers(int page, int size) {
Pageable pageable = PageRequest.of(page, size);
Page<UserEntity> userPage = userDao.findAll(pageable);
return userPage.getContent().stream()
.map(UserMapper::userEntityToUserAccount)
.collect(Collectors.toList());
}
@Override
public List<UserAccount> findUsersByCompany(UUID companyId, int page, int size) {
// For now, return all users filtered by company (needs DAO method)
Pageable pageable = PageRequest.of(page, size);
Page<UserEntity> userPage = userDao.findAll(pageable);
return userPage.getContent().stream()
.filter(user -> user.getCompany() != null && user.getCompany().getId().equals(companyId))
.map(UserMapper::userEntityToUserAccount)
.collect(Collectors.toList());
}
@Override
public long countAllUsers() {
return userDao.count();
}
@Override
public long countUsersByCompany(UUID companyId) {
// For now, count all and filter (needs DAO method for optimization)
return userDao.findAll().stream()
.filter(user -> user.getCompany() != null && user.getCompany().getId().equals(companyId))
.count();
}
}

View File

@ -41,9 +41,9 @@
</scm> </scm>
<properties> <properties>
<java.version>21</java.version> <java.version>23</java.version>
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target> <maven.compiler.target>23</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<org.projectlombok.version>1.18.36</org.projectlombok.version> <org.projectlombok.version>1.18.36</org.projectlombok.version>

View File

@ -1,148 +0,0 @@
#!/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}"

View File

@ -1 +0,0 @@
{"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"}