Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions Copilot-Processing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copilot Processing

## User Request
Implémenter les stories 3.3, 3.4 et 3.5 : Import npm Dependencies via File Upload, Import via Paste, et View Package List.

## Action Plan

### Phase 1: Backend Domain & Application Layer
- [x] 1.1 Create Package domain model (PackageId, Package)
- [x] 1.2 Create PackageRepository port
- [x] 1.3 Create LockfileParser port
- [x] 1.4 Create ImportPackagesFromLockfileUseCase (port + impl)
- [x] 1.5 Create ImportPackagesFromListUseCase (port + impl)
- [x] 1.6 Create ListCompanyPackagesUseCase (port + impl)
- [x] 1.7 Create DB migration for packages table

### Phase 2: Backend Infrastructure Layer
- [x] 2.1 Create PackageEntity + JPA Repository
- [x] 2.2 Create PackageRepositoryAdapter (PackageJpaRepository + PackageMapper)
- [x] 2.3 Create LockfileParserAdapter
- [x] 2.4 Create PackageResource REST endpoints + DTOs

### Phase 3: Frontend
- [x] 3.1 Create package API client
- [x] 3.2 Create FileDropzone component
- [x] 3.3 Create PastePackagesDialog component
- [x] 3.4 Create PackageCard component
- [x] 3.5 Create PackagesPage with search + infinite scroll
- [x] 3.6 Add route and navigation
- [x] 3.7 Update Get Started section with dynamic step states

### Phase 4: Tests
- [x] 4.1 Backend unit tests (PackageTest, LockfileParserAdapterTest, ImportPackagesFromLockfileUseCaseImplTest, ImportPackagesFromListUseCaseImplTest, ListCompanyPackagesUseCaseImplTest)
- [x] 4.2 Validate compilation (backend + frontend)
- [x] 4.3 Update GetCompanyDashboardUseCaseImpl + tests for hasPackages

## Summary

Implemented stories 3.3, 3.4, and 3.5 with full backend and frontend integration.

### Backend (27 new files)
**Domain:** Package, PackageId
**Ports:** PackageRepository, LockfileParser, ImportPackagesFromLockfileUseCase, ImportPackagesFromListUseCase, ListCompanyPackagesUseCase
**Use Cases:** ImportPackagesFromLockfileUseCaseImpl, ImportPackagesFromListUseCaseImpl, ListCompanyPackagesUseCaseImpl
**Infrastructure:** PackageEntity, PackageJpaRepository, PackageMapper, LockfileParserAdapter, PackageResource + 4 DTOs
**Migration:** V9__create_packages_table.sql

### Frontend (5 new files)
**Features:** packages/api.ts, FileDropzone.tsx, PastePackagesDialog.tsx, PackageCard.tsx
**Pages:** PackagesPage.tsx

### Modified Files
- GetCompanyDashboardUseCaseImpl.java (added hasPackages check)
- GetCompanyDashboardUseCaseImplTest.java (updated mocks)
- CompanyDashboardPage.tsx (dynamic Get Started steps with completion states)
- App.tsx (added /dashboard/packages route)

### Tests: 25 new tests, all passing
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.upkeep.application.port.in.pkg;

import java.util.List;

public interface ImportPackagesFromListUseCase {

ImportListResult execute(ImportFromListCommand command);

record ImportFromListCommand(
String companyId,
String customerId,
List<String> packageNames
) {}

record ImportListResult(
int importedCount,
int skippedCount,
int invalidCount,
List<String> importedNames,
List<String> skippedNames,
List<String> invalidNames
) {}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.upkeep.application.port.in.pkg;

import java.util.List;

public interface ImportPackagesFromLockfileUseCase {

ImportResult execute(ImportFromLockfileCommand command);

record ImportFromLockfileCommand(
String companyId,
String customerId,
String fileContent,
String filename
) {}

record ImportResult(
int importedCount,
int skippedCount,
int totalParsed,
List<String> importedNames,
List<String> skippedNames
) {}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.upkeep.application.port.in.pkg;

import java.util.List;

public interface ListCompanyPackagesUseCase {

PackageListResult execute(ListPackagesQuery query);

record ListPackagesQuery(
String companyId,
String customerId,
String search,
int page,
int size
) {}

record PackageListResult(
List<PackageItem> packages,
long totalCount,
int page,
int size
) {}

record PackageItem(
String id,
String name,
String registry,
String importedAt
) {}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.upkeep.application.port.out.pkg;

import java.util.List;

public interface LockfileParser {

List<String> parse(String content, String filename);

boolean supports(String filename);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.upkeep.application.port.out.pkg;

import com.upkeep.domain.model.company.CompanyId;
import com.upkeep.domain.model.pkg.Package;

import java.util.List;
import java.util.Set;

public interface PackageRepository {

void save(Package pkg);

void saveAll(List<Package> packages);

List<Package> findByCompanyId(CompanyId companyId, int offset, int limit);

List<Package> findByCompanyIdAndNameContaining(CompanyId companyId, String search, int offset, int limit);

long countByCompanyId(CompanyId companyId);

long countByCompanyIdAndNameContaining(CompanyId companyId, String search);

Set<String> findExistingNamesByCompanyId(CompanyId companyId, Set<String> names);

boolean existsByCompanyIdAndName(CompanyId companyId, String name);
}

Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.upkeep.application.usecase;

import com.upkeep.application.port.in.GetCompanyDashboardUseCase;
import com.upkeep.application.port.out.budget.BudgetRepository;
import com.upkeep.application.port.out.company.CompanyRepository;
import com.upkeep.application.port.out.membership.MembershipRepository;
import com.upkeep.application.port.out.pkg.PackageRepository;
import com.upkeep.domain.exception.CompanyNotFoundException;
import com.upkeep.domain.exception.MembershipNotFoundException;
import com.upkeep.domain.model.company.Company;
Expand All @@ -17,12 +19,18 @@ public class GetCompanyDashboardUseCaseImpl implements GetCompanyDashboardUseCas

private final CompanyRepository companyRepository;
private final MembershipRepository membershipRepository;
private final BudgetRepository budgetRepository;
private final PackageRepository packageRepository;

@Inject
public GetCompanyDashboardUseCaseImpl(CompanyRepository companyRepository,
MembershipRepository membershipRepository) {
MembershipRepository membershipRepository,
BudgetRepository budgetRepository,
PackageRepository packageRepository) {
this.companyRepository = companyRepository;
this.membershipRepository = membershipRepository;
this.budgetRepository = budgetRepository;
this.packageRepository = packageRepository;
}

@Override
Expand All @@ -37,11 +45,13 @@ public CompanyDashboard execute(GetCompanyDashboardQuery query) {
.orElseThrow(() -> new MembershipNotFoundException(query.customerId(), query.companyId()));

long totalMembers = membershipRepository.countByCompanyId(companyId);
boolean hasBudget = budgetRepository.existsByCompanyId(companyId);
boolean hasPackages = packageRepository.countByCompanyId(companyId) > 0;

DashboardStats stats = new DashboardStats(
(int) totalMembers,
false,
false,
hasBudget,
hasPackages,
false
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.upkeep.application.usecase;

import com.upkeep.application.port.in.pkg.ImportPackagesFromListUseCase;
import com.upkeep.application.port.out.membership.MembershipRepository;
import com.upkeep.application.port.out.pkg.PackageRepository;
import com.upkeep.domain.exception.MembershipNotFoundException;
import com.upkeep.domain.model.company.CompanyId;
import com.upkeep.domain.model.customer.CustomerId;
import com.upkeep.domain.model.pkg.Package;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@ApplicationScoped
public class ImportPackagesFromListUseCaseImpl implements ImportPackagesFromListUseCase {

private final PackageRepository packageRepository;
private final MembershipRepository membershipRepository;

@Inject
public ImportPackagesFromListUseCaseImpl(PackageRepository packageRepository,
MembershipRepository membershipRepository) {
this.packageRepository = packageRepository;
this.membershipRepository = membershipRepository;
}

@Override
@Transactional
public ImportListResult execute(ImportFromListCommand command) {
CompanyId companyId = CompanyId.from(command.companyId());
CustomerId customerId = CustomerId.from(command.customerId());

membershipRepository.findByCustomerIdAndCompanyId(customerId, companyId)
.orElseThrow(() -> new MembershipNotFoundException(command.customerId(), command.companyId()));

List<String> validNames = new ArrayList<>();
List<String> invalidNames = new ArrayList<>();

for (String rawName : command.packageNames()) {
String trimmed = rawName.trim();
if (trimmed.isEmpty()) {
continue;
}
if (Package.isValidNpmPackageName(trimmed)) {
validNames.add(trimmed);
} else {
invalidNames.add(trimmed);
}
}

Set<String> existingNames = packageRepository.findExistingNamesByCompanyId(companyId,
validNames.stream().collect(Collectors.toSet()));

List<String> importedNames = new ArrayList<>();
List<String> skippedNames = new ArrayList<>();
List<Package> toSave = new ArrayList<>();

for (String name : validNames) {
if (existingNames.contains(name)) {
skippedNames.add(name);
} else {
Package pkg = Package.create(companyId, name);
toSave.add(pkg);
importedNames.add(name);
}
}

if (!toSave.isEmpty()) {
packageRepository.saveAll(toSave);
}

return new ImportListResult(
importedNames.size(),
skippedNames.size(),
invalidNames.size(),
importedNames,
skippedNames,
invalidNames
);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.upkeep.application.usecase;

import com.upkeep.application.port.in.pkg.ImportPackagesFromLockfileUseCase;
import com.upkeep.application.port.out.membership.MembershipRepository;
import com.upkeep.application.port.out.pkg.LockfileParser;
import com.upkeep.application.port.out.pkg.PackageRepository;
import com.upkeep.domain.exception.MembershipNotFoundException;
import com.upkeep.domain.model.company.CompanyId;
import com.upkeep.domain.model.customer.CustomerId;
import com.upkeep.domain.model.pkg.Package;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@ApplicationScoped
public class ImportPackagesFromLockfileUseCaseImpl implements ImportPackagesFromLockfileUseCase {

private final PackageRepository packageRepository;
private final MembershipRepository membershipRepository;
private final LockfileParser lockfileParser;

@Inject
public ImportPackagesFromLockfileUseCaseImpl(PackageRepository packageRepository,
MembershipRepository membershipRepository,
LockfileParser lockfileParser) {
this.packageRepository = packageRepository;
this.membershipRepository = membershipRepository;
this.lockfileParser = lockfileParser;
}

@Override
@Transactional
public ImportResult execute(ImportFromLockfileCommand command) {
CompanyId companyId = CompanyId.from(command.companyId());
CustomerId customerId = CustomerId.from(command.customerId());

membershipRepository.findByCustomerIdAndCompanyId(customerId, companyId)
.orElseThrow(() -> new MembershipNotFoundException(command.customerId(), command.companyId()));

if (!lockfileParser.supports(command.filename())) {
throw new IllegalArgumentException("Unsupported lockfile format: " + command.filename());
}

List<String> parsedNames = lockfileParser.parse(command.fileContent(), command.filename());

Set<String> existingNames = packageRepository.findExistingNamesByCompanyId(companyId,
parsedNames.stream().collect(Collectors.toSet()));

List<String> importedNames = new ArrayList<>();
List<String> skippedNames = new ArrayList<>();
List<Package> toSave = new ArrayList<>();

for (String name : parsedNames) {
if (existingNames.contains(name)) {
skippedNames.add(name);
} else {
Package pkg = Package.create(companyId, name);
toSave.add(pkg);
importedNames.add(name);
}
}

if (!toSave.isEmpty()) {
packageRepository.saveAll(toSave);
}

return new ImportResult(
importedNames.size(),
skippedNames.size(),
parsedNames.size(),
importedNames,
skippedNames
);
}
}

Loading