From 9ec229d4dd50a0788089b9f2b5d215b3c17f8efd Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 31 Dec 2025 18:40:48 -0500 Subject: [PATCH 1/5] added claude.md for ai context and development guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 357 ++++++++++++++++++++++++++++++++++++++++++++++++++ ai-context.md | 0 2 files changed, 357 insertions(+) create mode 100644 CLAUDE.md create mode 100644 ai-context.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f795f41 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,357 @@ + +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Ballistic Builder is a **PCPartPicker-style platform for firearms**, starting with the AR-15 ecosystem. It's a Spring Boot 3.4.3 application that: +- Ingests merchant feeds (AvantLink) and normalizes product data +- Manages complex category mappings and part role classification +- Provides REST APIs for a consumer-facing Builder application +- Enables users to browse parts, compare prices, and assemble builds + +**Critical: This is NOT**: +- An e-commerce platform (does not sell products) +- A marketplace (does not process payments) +- A forum-first community +- An inventory management system + +**It IS**: +- An affiliate-driven aggregation platform +- A data normalization engine disguised as a UI +- Focused on accuracy, clarity, and trust + +Think: *"Build smarter rifles, not spreadsheets."* + +## Core Principles + +When working on this codebase, always prioritize: +- **Accuracy > Completeness** - Correct data is more important than comprehensive data +- **Idempotency > Speed** - Operations must be safely repeatable +- **Explicit data > Heuristics** - Prefer manual mappings over inference when accuracy matters +- **Long-term maintainability > Cleverness** - Code readability 6-12 months out matters more than clever abstractions +- **Incremental delivery** - Small team, phased solutions, clear migration paths + +Avoid introducing: +- Real-time inventory guarantees (merchant feeds are authoritative) +- Payment processing or checkout flows +- Legal/compliance risk (this is an aggregator, not a retailer) +- Over-engineered abstractions or premature optimization +- "Rewrite everything" solutions + +## Common Commands + +### Build & Run +```bash +# Clean build +mvn clean install + +# Run development server (port 8080) +mvn spring-boot:run + +# Run with Spring Boot DevTools hot reload +mvn spring-boot:run -Dspring-boot.run.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" +``` + +### Testing +```bash +# Run all tests +mvn test + +# Run specific test class +mvn test -Dtest=PlatformResolverTest + +# Run with coverage +mvn clean test jacoco:report +``` + +### Database +```bash +# PostgreSQL connection details in application.properties +# Default: jdbc:postgresql://r710.gofwd.group:5433/ss_builder +# User: postgres +``` + +### API Documentation +- Swagger UI: http://localhost:8080/swagger-ui.html +- OpenAPI spec: http://localhost:8080/v3/api-docs + +## Architecture Overview + +### Layered Architecture Pattern +``` +Controllers (REST API endpoints) + ↓ +Services (Business logic + validation) + ↓ +Repositories (Spring Data JPA) + ↓ +Models (JPA Entities) + ↓ +PostgreSQL Database +``` + +### Package Structure + +**Core Packages:** +- `catalog/classification/` - Platform resolution and product classification engine +- `controllers/` - REST API endpoints (v1 convention, admin panel) +- `enrichment/ai/` - AI-powered product data enhancement (OpenAI integration) +- `imports/` - CSV/TSV feed import functionality for merchant data +- `model/` - JPA entities (Product, User, Build, Brand, Merchant, etc.) +- `repos/` - Spring Data JPA repositories +- `security/` - JWT authentication, token validation, user details +- `services/` - Business logic layer (interface + impl pattern) +- `web/dto/` - Data Transfer Objects for API contracts +- `web/mapper/` - Entity to DTO conversion utilities + +### Key Technologies +- Java 21 +- Spring Boot 3.4.3 (Spring Data JPA, Spring Security, Spring Mail) +- PostgreSQL 42.7.7 with HikariCP connection pooling +- JWT authentication (JJWT 0.11.5) +- MinIO 8.4.3 (S3-compatible object storage for images) +- Apache Commons CSV 1.11.0 (feed processing) +- SpringDoc OpenAPI 2.8.5 (API documentation) + +## Critical Domain Concepts + +### Product Classification System + +**Platform Resolution**: Products are automatically classified to gun platforms (AR-15, AK-47, etc.) using rule-based logic in `catalog/classification/PlatformResolver`. Rules are stored in the `platform_rule` table and evaluated against product attributes (name, brand, MPN, UPC). + +**Part Role Mapping**: Products are assigned part roles (Upper Receiver, Barrel, etc.) through: +1. Automatic classification via `PartRoleRule` entities +2. Manual merchant category mappings in `MerchantCategoryMap` +3. Manual overrides with `platform_locked` flag + +**Category Normalization**: Raw merchant categories are mapped to `CanonicalCategory` through `CategoryMapping` entities. Unmapped categories are exposed in the admin UI for manual assignment. + +### Merchant Feed Ingestion (Critical) + +The import system is the **heart of the platform**. The `imports/` package handles CSV/TSV feed imports from AvantLink merchants with these critical characteristics: + +**Idempotency by Design**: +- Safe to re-run repeatedly without side effects +- Never duplicates products or offers +- Handles dirty, malformed, or incomplete feeds gracefully + +**Import Modes**: +1. **Full Import** - Products + offers (initial or full sync) +2. **Offer-Only Sync** - Price/stock refresh for existing products + +**Deduplication Rules**: +- Products deduped by **Brand + MPN** (primary key combination) +- UPC fallback for future enhancement +- Offers are **merchant-specific** (same product can have multiple offers) +- Offers track `firstSeenAt` and `lastSeenAt` timestamps + +**Process Flow**: +- Auto-detects delimiters (CSV/TSV) +- Creates or updates `Product` entities +- Upserts `ProductOffer` entities (price, stock, merchant URL) +- Tracks `FeedImport` status and history +- Logs errors without breaking entire import + +**Never Break Idempotency** - Any change to the import system must maintain the property that running imports multiple times produces the same result. + +### Build System + +Users create gun builds (`Build` entity) composed of `BuildItem` entities. Each build can be: +- Public (visible in community) +- Private (user-only) +- Linked to specific platforms +- Priced via aggregated `ProductOffer` data + +### Authentication & Authorization + +**JWT Flow**: +1. User logs in via `/api/auth/login` +2. `JwtService` generates access token (48-hour expiry) +3. Token stored in `AuthToken` table +4. `JwtAuthenticationFilter` validates token on each request +5. Principal stored as UUID string in SecurityContext + +**Roles**: USER, ADMIN (role-based access control via Spring Security) + +**Magic Links**: Users can request passwordless login links (30-day token expiry) + +## Performance Considerations + +### Recent Optimizations +- **API batch sizes reduced**: Catalog endpoints previously returned 2000 products per call, now paginated to ~48 items +- **DTO optimization**: `ProductDTO` streamlined for faster serialization +- **N+1 query prevention**: Use `@EntityGraph` annotations to eagerly fetch relations + +### Caching Strategy +- Spring Caching enabled globally (`@EnableCaching`) +- Product details cached in `ProductV1Controller` +- Clear cache after imports or admin updates + +### Database Query Patterns +- Use `JpaSpecificationExecutor` for dynamic filtering +- Complex aggregations use native PostgreSQL queries +- HikariCP max connection lifetime: 10 minutes + +## API Design Patterns + +### Endpoint Conventions +- Public API: `/api/v1/{resource}` +- Admin API: `/api/v1/admin/{resource}` +- Auth endpoints: `/api/auth/{action}` +- Legacy endpoints return 410 Gone (intentionally deprecated) + +### Request/Response Flow +1. Controller receives request with DTOs +2. Validates input (Spring Validation) +3. Maps DTO to entity via mapper +4. Calls service layer (business logic) +5. Service interacts with repositories +6. Maps entity back to DTO +7. Returns JSON response + +### Authorization Patterns +- JWT token in `Authorization: Bearer ` header +- Role checks via `@PreAuthorize("hasRole('ADMIN')")` or SecurityConfig rules +- User identity extracted from SecurityContext via `JwtAuthenticationFilter` + +## Common Development Patterns + +### Adding a New API Endpoint +1. Create DTO classes in `web/dto/` (request + response) +2. Create/update service interface in `services/` +3. Implement service in `services/impl/` +4. Create controller in `controllers/` (follow naming: `{Resource}V1Controller`) +5. Inject service via constructor +6. Add authorization rules in `SecurityConfig` or `@PreAuthorize` +7. Document with SpringDoc annotations (`@Operation`, `@ApiResponse`) + +### Entity Relationships +- Use `@EntityGraph` to prevent N+1 queries +- Soft delete pattern: `deletedAt` field (filter `WHERE deleted_at IS NULL`) +- Temporal tracking: `createdAt`, `updatedAt` via `@PrePersist`/`@PreUpdate` +- Avoid bidirectional relationships unless necessary + +### Service Layer Pattern +- Define interface: `public interface BrandService { ... }` +- Implement: `@Service public class BrandServiceImpl implements BrandService` +- Use constructor injection for dependencies +- Handle business logic, validation, and transactions + +## Configuration + +### Environment-Specific Settings +Key properties in `src/main/resources/application.properties`: + +```properties +# Database +spring.datasource.url=jdbc:postgresql://r710.gofwd.group:5433/ss_builder + +# JWT +security.jwt.secret=ballistic-test-secret-key-... +security.jwt.access-token-minutes=2880 # 48 hours + +# MinIO (image storage) +minio.endpoint=https://minioapi.dev.gofwd.group +minio.bucket=battlbuilders +minio.public-base-url=https://minio.dev.gofwd.group + +# Email (SMTP) +spring.mail.host=mail.goforwardmail.com +spring.mail.username=info@battl.builders + +# AI/OpenAI +ai.openai.apiKey=sk-proj-... +ai.openai.model=gpt-4.1-mini +ai.minConfidence=0.75 + +# Feature Flags +app.api.legacy.enabled=false +app.beta.captureOnly=true +app.email.outbound-enabled=true +``` + +### CORS Configuration +Allowed origins configured in `CorsConfig`: +- `http://localhost:3000` (React dev) +- `http://localhost:8080`, `8070`, `4200`, `4201` + +## Important Notes + +### Legacy Code +- `ProductController` intentionally returns 410 Gone (deprecated API v0) +- Old `Account` model coexists with newer `User` model +- JSP support included but not actively used + +### Security Warnings +- Credentials in `application.properties` should use environment variables in production +- Rotate OpenAI API key regularly +- Use secrets manager for email passwords +- Ensure magic link tokens have short expiration + +### Multi-Tenancy +- System supports multiple merchants via `Merchant` entity +- Category mappings are merchant-specific (`MerchantCategoryMap`) +- Platform rules engine allows per-merchant classification overrides + +### Testing Strategy +- TestNG framework included +- Minimal test coverage currently +- Key test: `PlatformResolverTest` for classification logic +- Integration tests should cover feed import flows + +## Development Constraints & Guidelines + +### Team Context +This is a **small team** building for **longevity and credibility**. Code decisions should account for: +- Code readability 6-12 months from now +- Incremental delivery over big-bang rewrites +- Clear migration paths when refactoring +- Minimal external dependencies unless truly necessary + +### When Proposing Changes +- **Feature requests**: Propose phased, minimal solutions +- **Refactors**: Explain tradeoffs and provide migration steps +- **Debugging**: Reason from architecture first (check layers: controller → service → repo → entity) +- **Design questions**: Follow the PCPartPicker mental model +- **Scaling concerns**: Assume success but move incrementally + +### What NOT to Suggest +- Rewriting large portions of the codebase +- Adding frameworks for problems that don't exist yet +- Magical inference engines or hidden behavior +- Solutions that introduce legal/compliance risk +- Features that assume this is an e-commerce checkout platform + +### Frontend Assumptions +The Next.js/React frontend depends on this backend for: +- Normalized APIs (backend owns business logic) +- Part role authoritative data +- Backend-driven compatibility rules +- No duplication of backend logic in frontend + +## How to Work with This Codebase + +### To Understand a Feature +1. Find the REST endpoint in `controllers/` +2. Trace the service call in `services/` (interface → impl) +3. Check repository queries in `repos/` +4. Review model relationships in `model/` +5. Check DTOs in `web/dto/` for request/response contracts + +### To Debug an Issue +1. Check controller layer for request validation +2. Verify service layer business logic +3. Inspect repository queries (check for N+1) +4. Review entity relationships and fetch strategies +5. Check application.properties for configuration issues +6. Review logs for SQL queries (Hibernate logging) + +### Red Flags to Watch For +- Breaking idempotency in import system +- Introducing N+1 query problems +- Adding credentials to application.properties +- Creating bidirectional JPA relationships without careful consideration +- Bypassing the service layer from controllers +- Duplicating business logic in DTOs or controllers diff --git a/ai-context.md b/ai-context.md new file mode 100644 index 0000000..e69de29 From babba2757b645964f2715ceae279ccefa5d5cb32 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 31 Dec 2025 18:41:23 -0500 Subject: [PATCH 2/5] added ai context for openai and claude --- ai-context.md | 195 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/ai-context.md b/ai-context.md index e69de29..b0833d4 100644 --- a/ai-context.md +++ b/ai-context.md @@ -0,0 +1,195 @@ +# 🤖 Battl Builder — AI Context & Guardrails + +> **Purpose:** +> This document provides persistent context for AI assistants working on the **Battl Builder** project. +> It defines the product vision, architecture, constraints, and expectations so AI outputs remain aligned, safe, and useful. + +--- + +## 1. Project Overview + +**Battl Builder** is a PCPartPicker-style platform for firearms, starting with the **AR-15 ecosystem**. + +Users can: +- Browse firearm parts by role (upper, lower, barrel, etc.) +- Compare prices across multiple merchants +- Assemble builds using a visual Builder UI +- Share builds publicly + +Battl Builder: +- **Does not sell products** +- **Does not process payments** +- Is **affiliate-driven** (AvantLink) +- Focuses on **clarity, accuracy, and trust** + +Think: *“Build smarter rifles, not spreadsheets.”* + +--- + +## 2. Product Philosophy + +### Core Principles +- **Accuracy > Completeness** +- **Idempotency > Speed** +- **Explicit data > heuristics** +- **Long-term maintainability > cleverness** + +### Non-Goals +AI should NOT assume this project is: +- An e-commerce checkout platform +- A firearm marketplace +- A forum-first community +- A content or media brand (for now) + +Avoid suggestions that introduce: +- Real-time inventory guarantees +- Payment processing +- Legal/compliance risk +- Over-engineered abstractions + +--- + +## 3. Current State (Beta) + +### What Exists +- User authentication +- Functional Builder UI +- Dedicated part pages +- Basic builds community +- 2 merchants (AvantLink) +- Production Spring Boot backend + +### What’s Intentionally Light +- Compatibility logic (rule-based, evolving) +- Social features +- Price history & analytics +- Public build discovery + +This is a **real beta**, not a mock project. + +--- + +## 4. Backend Architecture + +### Tech Stack +- **Java 17** +- **Spring Boot 3.x** +- **PostgreSQL** +- **Hibernate / JPA** +- **Maven** +- **REST APIs** +- **Docker (local + infra)** + +### Core Responsibilities +- Merchant feed ingestion (CSV / TSV) +- Product normalization & deduplication +- Offer upserts (price, stock, URLs) +- Category → Part Role mapping +- Platform inference (AR-15 today) +- Supplying clean data to the Builder UI + +--- + +## 5. Import Pipeline (Critical Context) + +The import system is the **heart of the platform**. + +### Key Characteristics +- Idempotent by design +- Safe to re-run repeatedly +- Handles dirty, malformed feeds +- Never duplicates products or offers + +### Import Modes +- **Full Import:** products + offers +- **Offer-Only Sync:** price/stock refresh + +### Deduplication Rules +- Products deduped by **Brand + MPN** (UPC fallback later) +- Offers are **merchant-specific** +- Offers track: + - `firstSeenAt` + - `lastSeenAt` + +⚠️ **Never introduce logic that breaks idempotency.** +⚠️ **Never assume merchant data is clean or complete.** + +--- + +## 6. Frontend Expectations + +- Next.js / React +- Backend owns business logic +- Frontend consumes normalized APIs +- No hardcoded compatibility rules +- No duplication of backend logic + +The Builder UI assumes: +- Part roles are authoritative +- Compatibility is backend-driven +- Data is already normalized + +--- + +## 7. Infrastructure & Dev Environment + +- PostgreSQL (Docker or local) +- Spring Boot runs on `:8080` +- CI builds on push / PR +- Local dev favors simplicity over cleverness + +--- + +## 8. Constraints You Must Respect + +AI suggestions must account for: +- Small team +- Incremental delivery +- Clear migration paths +- Code readability 6–12 months out + +Avoid: +- “Rewrite everything” answers +- Premature abstractions +- Magical inference engines +- Hidden or implicit behavior + +--- + +## 9. How AI Should Respond + +When assisting on this project: + +- **Feature requests** → propose phased, minimal solutions +- **Refactors** → explain tradeoffs and migration steps +- **Debugging** → reason from architecture first +- **Design** → follow PCPartPicker mental models +- **Scaling** → assume success, move incrementally + +If something is unclear: +- Ask **one focused question** +- Otherwise state assumptions and proceed + +--- + +## 10. Mental Model Summary + +Battl Builder is: + +- A **builder, not a store** +- A **data normalization engine disguised as a UI** +- Opinionated where it matters +- Flexible where it counts +- Built for **credibility and longevity** + +AI assistance should help the project grow **cleanly, safely, and intelligently**. + +--- + +### ✅ Recommended Usage +Paste this file into: +- Cursor / Copilot project context +- ChatGPT system prompt +- Claude project memory +- `/docs/ai-context.md` +- \ No newline at end of file From d52dee105cfb6da5cca614663dd13cb68a2ad77e Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 2 Jan 2026 09:50:33 -0500 Subject: [PATCH 3/5] added server side price sorting endpoints --- .../catalog/query/ProductWithBestPrice.java | 15 +++++++ .../controllers/ProductV1Controller.java | 18 ++++++-- .../battlbuilder/repos/ProductRepository.java | 23 ++++++++++ .../services/ProductQueryService.java | 13 ++++-- .../impl/ProductQueryServiceImpl.java | 42 +++++++++++++++---- .../web/dto/catalog/ProductSort.java | 15 +++++++ 6 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 src/main/java/group/goforward/battlbuilder/catalog/query/ProductWithBestPrice.java create mode 100644 src/main/java/group/goforward/battlbuilder/web/dto/catalog/ProductSort.java diff --git a/src/main/java/group/goforward/battlbuilder/catalog/query/ProductWithBestPrice.java b/src/main/java/group/goforward/battlbuilder/catalog/query/ProductWithBestPrice.java new file mode 100644 index 0000000..21e1542 --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/catalog/query/ProductWithBestPrice.java @@ -0,0 +1,15 @@ +package group.goforward.battlbuilder.catalog.query; + +import java.math.BigDecimal; +import java.util.UUID; + +public interface ProductWithBestPrice { + Long getId(); + UUID getUuid(); + String getName(); + String getSlug(); + String getPlatform(); + String getPartRole(); + + BigDecimal getBestPrice(); // derived +} diff --git a/src/main/java/group/goforward/battlbuilder/controllers/ProductV1Controller.java b/src/main/java/group/goforward/battlbuilder/controllers/ProductV1Controller.java index 7d4fa23..5799275 100644 --- a/src/main/java/group/goforward/battlbuilder/controllers/ProductV1Controller.java +++ b/src/main/java/group/goforward/battlbuilder/controllers/ProductV1Controller.java @@ -3,12 +3,13 @@ package group.goforward.battlbuilder.controllers; import group.goforward.battlbuilder.services.ProductQueryService; import group.goforward.battlbuilder.web.dto.ProductOfferDto; import group.goforward.battlbuilder.web.dto.ProductSummaryDto; +import group.goforward.battlbuilder.web.dto.catalog.ProductSort; import org.springframework.cache.annotation.Cacheable; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -23,17 +24,26 @@ public class ProductV1Controller { this.productQueryService = productQueryService; } + /** + * Product list endpoint + * Example: + * /api/v1/products?platform=AR-15&partRoles=upper-receiver&priceSort=price_asc&page=0&size=50 + * + * NOTE: do NOT use `sort=` here — Spring reserves it for Pageable sorting. + */ @GetMapping @Cacheable( value = "gunbuilderProductsV1", - key = "#platform + '::' + (#partRoles == null ? 'ALL' : #partRoles.toString()) + '::' + #pageable.pageNumber + '::' + #pageable.pageSize" + key = "#platform + '::' + (#partRoles == null ? 'ALL' : #partRoles) + '::' + #priceSort + '::' + #pageable.pageNumber + '::' + #pageable.pageSize" ) public Page getProducts( @RequestParam(defaultValue = "AR-15") String platform, @RequestParam(required = false, name = "partRoles") List partRoles, + @RequestParam(name = "priceSort", defaultValue = "price_asc") String priceSort, @PageableDefault(size = 50) Pageable pageable ) { - return productQueryService.getProductsPage(platform, partRoles, pageable); + ProductSort sortEnum = ProductSort.from(priceSort); + return productQueryService.getProductsPage(platform, partRoles, pageable, sortEnum); } @GetMapping("/{id}/offers") diff --git a/src/main/java/group/goforward/battlbuilder/repos/ProductRepository.java b/src/main/java/group/goforward/battlbuilder/repos/ProductRepository.java index 9c93a27..9b11fb9 100644 --- a/src/main/java/group/goforward/battlbuilder/repos/ProductRepository.java +++ b/src/main/java/group/goforward/battlbuilder/repos/ProductRepository.java @@ -3,6 +3,7 @@ package group.goforward.battlbuilder.repos; import group.goforward.battlbuilder.model.ImportStatus; import group.goforward.battlbuilder.model.Brand; import group.goforward.battlbuilder.model.Product; +import group.goforward.battlbuilder.catalog.query.ProductWithBestPrice; import group.goforward.battlbuilder.repos.projections.CatalogRow; import group.goforward.battlbuilder.web.dto.catalog.CatalogOptionRow; @@ -625,6 +626,28 @@ ORDER BY productCount DESC Pageable pageable ); + // ------------------------------------------------- + // Best Price + // ------------------------------------------------- + @Query(""" + select + p.id as id, + p.uuid as uuid, + p.name as name, + p.slug as slug, + p.platform as platform, + p.partRole as partRole, + min(o.price) as bestPrice + from Product p + join ProductOffer o on o.product.id = p.id + where (:platform is null or p.platform = :platform) + and o.inStock = true + group by p.id, p.uuid, p.name, p.slug, p.platform, p.partRole + """) + Page findProductsWithBestPriceInStock( + @Param("platform") String platform, + Pageable pageable + ); } diff --git a/src/main/java/group/goforward/battlbuilder/services/ProductQueryService.java b/src/main/java/group/goforward/battlbuilder/services/ProductQueryService.java index 0ca20d1..f6f9c5a 100644 --- a/src/main/java/group/goforward/battlbuilder/services/ProductQueryService.java +++ b/src/main/java/group/goforward/battlbuilder/services/ProductQueryService.java @@ -2,12 +2,12 @@ package group.goforward.battlbuilder.services; import group.goforward.battlbuilder.web.dto.ProductOfferDto; import group.goforward.battlbuilder.web.dto.ProductSummaryDto; - -import java.util.List; - +import group.goforward.battlbuilder.web.dto.catalog.ProductSort; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import java.util.List; + public interface ProductQueryService { List getProducts(String platform, List partRoles); @@ -16,5 +16,10 @@ public interface ProductQueryService { ProductSummaryDto getProductById(Integer productId); - Page getProductsPage(String platform, List partRoles, Pageable pageable); + Page getProductsPage( + String platform, + List partRoles, + Pageable pageable, + ProductSort sort + ); } \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/services/impl/ProductQueryServiceImpl.java b/src/main/java/group/goforward/battlbuilder/services/impl/ProductQueryServiceImpl.java index 49f75ca..c4ac5bd 100644 --- a/src/main/java/group/goforward/battlbuilder/services/impl/ProductQueryServiceImpl.java +++ b/src/main/java/group/goforward/battlbuilder/services/impl/ProductQueryServiceImpl.java @@ -8,7 +8,7 @@ import group.goforward.battlbuilder.services.ProductQueryService; import group.goforward.battlbuilder.web.dto.ProductOfferDto; import group.goforward.battlbuilder.web.dto.ProductSummaryDto; import group.goforward.battlbuilder.web.mapper.ProductMapper; -import org.springframework.stereotype.Service; +import group.goforward.battlbuilder.web.dto.catalog.ProductSort; import java.math.BigDecimal; import java.util.*; @@ -16,6 +16,8 @@ import java.util.stream.Collectors; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + import java.util.stream.Collectors; import java.util.Collections; @@ -79,24 +81,37 @@ public class ProductQueryServiceImpl implements ProductQueryService { } @Override - public Page getProductsPage(String platform, List partRoles, Pageable pageable) { + public Page getProductsPage( + String platform, + List partRoles, + Pageable pageable, + ProductSort sort + ) { final boolean allPlatforms = platform != null && platform.equalsIgnoreCase("ALL"); + // IMPORTANT: ignore Pageable sorting (because we are doing our own "best price" logic) + // If the client accidentally passes ?sort=..., Spring Data will try ordering by a Product field. + // We'll strip it to be safe. + Pageable safePageable = pageable; + if (pageable != null && pageable.getSort() != null && pageable.getSort().isSorted()) { + safePageable = Pageable.ofSize(pageable.getPageSize()).withPage(pageable.getPageNumber()); + } + Page productPage; if (partRoles == null || partRoles.isEmpty()) { productPage = allPlatforms - ? productRepository.findAllWithBrand(pageable) - : productRepository.findByPlatformWithBrand(platform, pageable); + ? productRepository.findAllWithBrand(safePageable) + : productRepository.findByPlatformWithBrand(platform, safePageable); } else { productPage = allPlatforms - ? productRepository.findByPartRoleInWithBrand(partRoles, pageable) - : productRepository.findByPlatformAndPartRoleInWithBrand(platform, partRoles, pageable); + ? productRepository.findByPartRoleInWithBrand(partRoles, safePageable) + : productRepository.findByPlatformAndPartRoleInWithBrand(platform, partRoles, safePageable); } List products = productPage.getContent(); if (products.isEmpty()) { - return Page.empty(pageable); + return Page.empty(safePageable); } List productIds = products.stream().map(Product::getId).toList(); @@ -108,6 +123,7 @@ public class ProductQueryServiceImpl implements ProductQueryService { .filter(o -> o.getProduct() != null && o.getProduct().getId() != null) .collect(Collectors.groupingBy(o -> o.getProduct().getId())); + // Build DTOs (same as before) List dtos = products.stream() .map(p -> { List offersForProduct = @@ -122,7 +138,17 @@ public class ProductQueryServiceImpl implements ProductQueryService { }) .toList(); - return new PageImpl<>(dtos, pageable, productPage.getTotalElements()); + // Phase 3 "server-side sort by price" (within the page for now) + Comparator byPriceNullLast = + Comparator.comparing(ProductSummaryDto::getPrice, Comparator.nullsLast(Comparator.naturalOrder())); + + if (sort == ProductSort.PRICE_DESC) { + dtos = dtos.stream().sorted(byPriceNullLast.reversed()).toList(); + } else { + dtos = dtos.stream().sorted(byPriceNullLast).toList(); + } + + return new PageImpl<>(dtos, safePageable, productPage.getTotalElements()); } // diff --git a/src/main/java/group/goforward/battlbuilder/web/dto/catalog/ProductSort.java b/src/main/java/group/goforward/battlbuilder/web/dto/catalog/ProductSort.java new file mode 100644 index 0000000..d43d532 --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/web/dto/catalog/ProductSort.java @@ -0,0 +1,15 @@ +package group.goforward.battlbuilder.web.dto.catalog; + +public enum ProductSort { + PRICE_ASC, + PRICE_DESC; + + public static ProductSort from(String raw) { + if (raw == null) return PRICE_ASC; + String s = raw.trim().toLowerCase(); + return switch (s) { + case "price_desc", "price-desc", "desc" -> PRICE_DESC; + default -> PRICE_ASC; + }; + } +} \ No newline at end of file From ae209abb77d778e0422ecfabf7d41986d6406821 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 5 Jan 2026 13:43:34 -0500 Subject: [PATCH 4/5] fixes for platforms in admin pages. --- .../admin/AdminPlatformController.java | 75 +++++++++++++++---- .../services/admin/AdminProductService.java | 9 +-- .../admin/impl/AdminProductServiceImpl.java | 56 +++++++++++++- .../web/admin/AdminProductController.java | 12 +-- .../web/dto/admin/BulkUpdateResult.java | 6 ++ .../web/dto/admin/PlatformCreateRequest.java | 16 ++++ .../dto/admin/ProductBulkUpdateRequest.java | 9 ++- 7 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 src/main/java/group/goforward/battlbuilder/web/dto/admin/BulkUpdateResult.java create mode 100644 src/main/java/group/goforward/battlbuilder/web/dto/admin/PlatformCreateRequest.java diff --git a/src/main/java/group/goforward/battlbuilder/controllers/admin/AdminPlatformController.java b/src/main/java/group/goforward/battlbuilder/controllers/admin/AdminPlatformController.java index 307ef24..96491a5 100644 --- a/src/main/java/group/goforward/battlbuilder/controllers/admin/AdminPlatformController.java +++ b/src/main/java/group/goforward/battlbuilder/controllers/admin/AdminPlatformController.java @@ -1,4 +1,4 @@ -package group.goforward.battlbuilder.controllers; +package group.goforward.battlbuilder.controllers.admin; import group.goforward.battlbuilder.model.Platform; import group.goforward.battlbuilder.repos.PlatformRepository; @@ -36,21 +36,70 @@ public class AdminPlatformController { .toList(); } - @PostMapping("/add") - public ResponseEntity createPlatform(@RequestBody Platform platform) { + /** + * Create new platform (RESTful) + * POST /api/platforms + */ + @PostMapping + public ResponseEntity create(@RequestBody Platform platform) { + // Normalize key so we don’t end up with Ak47 / ak47 / AK-47 variants + if (platform.getKey() != null) { + platform.setKey(platform.getKey().trim().toUpperCase()); + } + + // Optional: if label empty, default to key + if (platform.getLabel() == null || platform.getLabel().trim().isEmpty()) { + platform.setLabel(platform.getKey()); + } + + // Default active = true if omitted + if (platform.getIsActive() == null) { + platform.setIsActive(true); + } + platform.setCreatedAt(OffsetDateTime.now()); platform.setUpdatedAt(OffsetDateTime.now()); + Platform created = platformRepository.save(platform); - return ResponseEntity.status(HttpStatus.CREATED).body(created); + + return ResponseEntity.status(HttpStatus.CREATED).body( + new PlatformDto( + created.getId(), + created.getKey(), + created.getLabel(), + created.getCreatedAt(), + created.getUpdatedAt(), + created.getIsActive() + ) + ); } - @DeleteMapping("/delete/{id}") - public ResponseEntity deletePlatform(@PathVariable Integer id) { - return platformRepository.findById(id) - .map(platform -> { - platformRepository.deleteById(id); - return ResponseEntity.noContent().build(); - }) - .orElse(ResponseEntity.notFound().build()); + /** + * (Optional) keep old endpoint temporarily so nothing breaks + */ + @PostMapping("/add") + public ResponseEntity createViaAdd(@RequestBody Platform platform) { + return create(platform); } -} + + /** + * Delete platform (RESTful) + * DELETE /api/platforms/{id} + */ + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Integer id) { + if (!platformRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + platformRepository.deleteById(id); + return ResponseEntity.noContent().build(); + } + + /** + * (Optional) keep old delete route temporarily + */ + @DeleteMapping("/delete/{id}") + public ResponseEntity deleteLegacy(@PathVariable Integer id) { + return delete(id); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/services/admin/AdminProductService.java b/src/main/java/group/goforward/battlbuilder/services/admin/AdminProductService.java index 9cbf4c7..8158670 100644 --- a/src/main/java/group/goforward/battlbuilder/services/admin/AdminProductService.java +++ b/src/main/java/group/goforward/battlbuilder/services/admin/AdminProductService.java @@ -3,16 +3,13 @@ package group.goforward.battlbuilder.services.admin; import group.goforward.battlbuilder.web.dto.admin.AdminProductSearchRequest; import group.goforward.battlbuilder.web.dto.admin.ProductAdminRowDto; import group.goforward.battlbuilder.web.dto.admin.ProductBulkUpdateRequest; +import group.goforward.battlbuilder.web.dto.admin.BulkUpdateResult; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface AdminProductService { + Page search(AdminProductSearchRequest request, Pageable pageable); - Page search( - AdminProductSearchRequest request, - Pageable pageable - ); - - int bulkUpdate(ProductBulkUpdateRequest request); + BulkUpdateResult bulkUpdate(ProductBulkUpdateRequest request); } \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/services/admin/impl/AdminProductServiceImpl.java b/src/main/java/group/goforward/battlbuilder/services/admin/impl/AdminProductServiceImpl.java index 1f242aa..3d24752 100644 --- a/src/main/java/group/goforward/battlbuilder/services/admin/impl/AdminProductServiceImpl.java +++ b/src/main/java/group/goforward/battlbuilder/services/admin/impl/AdminProductServiceImpl.java @@ -5,6 +5,7 @@ import group.goforward.battlbuilder.repos.ProductRepository; import group.goforward.battlbuilder.services.admin.AdminProductService; import group.goforward.battlbuilder.specs.ProductSpecifications; import group.goforward.battlbuilder.web.dto.admin.AdminProductSearchRequest; +import group.goforward.battlbuilder.web.dto.admin.BulkUpdateResult; import group.goforward.battlbuilder.web.dto.admin.ProductAdminRowDto; import group.goforward.battlbuilder.web.dto.admin.ProductBulkUpdateRequest; @@ -38,28 +39,75 @@ public class AdminProductServiceImpl implements AdminProductService { } @Override - public int bulkUpdate(ProductBulkUpdateRequest request) { + public BulkUpdateResult bulkUpdate(ProductBulkUpdateRequest request) { var products = productRepository.findAllById(request.getProductIds()); - products.forEach(p -> { + int updated = 0; + int skippedLocked = 0; + + for (var p : products) { + boolean changed = false; + + // --- straightforward fields --- if (request.getVisibility() != null) { p.setVisibility(request.getVisibility()); + changed = true; } if (request.getStatus() != null) { p.setStatus(request.getStatus()); + changed = true; } if (request.getBuilderEligible() != null) { p.setBuilderEligible(request.getBuilderEligible()); + changed = true; } if (request.getAdminLocked() != null) { p.setAdminLocked(request.getAdminLocked()); + changed = true; } if (request.getAdminNote() != null) { p.setAdminNote(request.getAdminNote()); + changed = true; } - }); + + // --- platform update with lock semantics --- + if (request.getPlatform() != null) { + boolean isLocked = Boolean.TRUE.equals(p.getPlatformLocked()); + boolean override = Boolean.TRUE.equals(request.getPlatformLocked()); // request says "I'm allowed to touch locked ones" + + if (isLocked && !override) { + skippedLocked++; + } else { + if (!request.getPlatform().equals(p.getPlatform())) { + p.setPlatform(request.getPlatform()); + changed = true; + } + } + } + + // --- apply platformLocked toggle (even if platform isn't being changed) --- + if (request.getPlatformLocked() != null) { + if (!request.getPlatformLocked().equals(p.getPlatformLocked())) { + p.setPlatformLocked(request.getPlatformLocked()); + changed = true; + } + } + + if (changed) updated++; + } productRepository.saveAll(products); - return products.size(); + productRepository.flush(); // ✅ ensures UPDATEs are executed now + + var check = productRepository.findAllById(request.getProductIds()); + for (var p : check) { + System.out.println( + "AFTER FLUSH id=" + p.getId() + + " platform=" + p.getPlatform() + + " platformLocked=" + p.getPlatformLocked() + ); + } + + return new BulkUpdateResult(updated, skippedLocked); } } \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/web/admin/AdminProductController.java b/src/main/java/group/goforward/battlbuilder/web/admin/AdminProductController.java index df5e539..568b888 100644 --- a/src/main/java/group/goforward/battlbuilder/web/admin/AdminProductController.java +++ b/src/main/java/group/goforward/battlbuilder/web/admin/AdminProductController.java @@ -3,6 +3,7 @@ package group.goforward.battlbuilder.web.admin; import group.goforward.battlbuilder.services.admin.AdminProductService; import group.goforward.battlbuilder.web.dto.admin.AdminProductSearchRequest; import group.goforward.battlbuilder.web.dto.admin.ProductBulkUpdateRequest; +import group.goforward.battlbuilder.web.dto.admin.BulkUpdateResult; import group.goforward.battlbuilder.web.dto.admin.ProductAdminRowDto; import org.springframework.data.domain.Page; @@ -36,10 +37,11 @@ public class AdminProductController { * Bulk admin actions (disable, hide, lock, etc.) */ @PatchMapping("/bulk") - public Map bulkUpdate( - @RequestBody ProductBulkUpdateRequest request - ) { - int updated = adminProductService.bulkUpdate(request); - return Map.of("updatedCount", updated); + public Map bulkUpdate(@RequestBody ProductBulkUpdateRequest request) { + BulkUpdateResult result = adminProductService.bulkUpdate(request); + return Map.of( + "updatedCount", result.updatedCount(), + "skippedLockedCount", result.skippedLockedCount() + ); } } \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/web/dto/admin/BulkUpdateResult.java b/src/main/java/group/goforward/battlbuilder/web/dto/admin/BulkUpdateResult.java new file mode 100644 index 0000000..076d878 --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/web/dto/admin/BulkUpdateResult.java @@ -0,0 +1,6 @@ +package group.goforward.battlbuilder.web.dto.admin; + +public record BulkUpdateResult( + int updatedCount, + int skippedLockedCount +) {} diff --git a/src/main/java/group/goforward/battlbuilder/web/dto/admin/PlatformCreateRequest.java b/src/main/java/group/goforward/battlbuilder/web/dto/admin/PlatformCreateRequest.java new file mode 100644 index 0000000..e4569f3 --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/web/dto/admin/PlatformCreateRequest.java @@ -0,0 +1,16 @@ +package group.goforward.battlbuilder.web.dto.admin; + +public class PlatformCreateRequest { + private String key; + private String label; + private Boolean isActive; + + public String getKey() { return key; } + public void setKey(String key) { this.key = key; } + + public String getLabel() { return label; } + public void setLabel(String label) { this.label = label; } + + public Boolean getIsActive() { return isActive; } + public void setIsActive(Boolean isActive) { this.isActive = isActive; } +} diff --git a/src/main/java/group/goforward/battlbuilder/web/dto/admin/ProductBulkUpdateRequest.java b/src/main/java/group/goforward/battlbuilder/web/dto/admin/ProductBulkUpdateRequest.java index e235657..4098c38 100644 --- a/src/main/java/group/goforward/battlbuilder/web/dto/admin/ProductBulkUpdateRequest.java +++ b/src/main/java/group/goforward/battlbuilder/web/dto/admin/ProductBulkUpdateRequest.java @@ -8,7 +8,8 @@ import java.util.Set; public class ProductBulkUpdateRequest { private Set productIds; - + private String platform; + private Boolean platformLocked; private ProductVisibility visibility; private ProductStatus status; @@ -36,4 +37,10 @@ public class ProductBulkUpdateRequest { public String getAdminNote() { return adminNote; } public void setAdminNote(String adminNote) { this.adminNote = adminNote; } + + public String getPlatform() { return platform; } + public void setPlatform(String platform) { this.platform = platform; } + + public Boolean getPlatformLocked() { return platformLocked; } + public void setPlatformLocked(Boolean platformLocked) { this.platformLocked = platformLocked; } } \ No newline at end of file From 97c28e94b80220a7848abf77ec9fdfad09491dfa Mon Sep 17 00:00:00 2001 From: Don Strawsburg Date: Mon, 5 Jan 2026 17:29:13 -0500 Subject: [PATCH 5/5] moving packages --- .../catalog/query/package-info.java | 4 + .../battlbuilder/cli/package-info.java | 4 + .../{utils => common}/ApiResponse.java | 148 ++++++++-------- .../{utils => common}/Counter.java | 40 ++--- .../{utils => common}/package-info.java | 0 .../CacheConfig.java | 42 ++--- .../{configuration => config}/CorsConfig.java | 166 +++++++++--------- .../{configuration => config}/JpaConfig.java | 18 +- .../MinioConfig.java | 0 .../PasswordConfig.java | 0 .../SecurityConfig.java | 0 .../package-info.java | 0 .../{ => api/v1}/AuthController.java | 0 .../api/{ => v1}/BrandController.java | 100 +++++------ .../api/{ => v1}/BuildController.java | 68 +++---- .../{ => api/v1}/BuildV1Controller.java | 0 .../v1}/BuilderBootstrapController.java | 0 .../{ => api/v1}/CatalogController.java | 0 .../{ => api/v1}/CategoryController.java | 0 .../{ => api/v1}/ImportController.java | 78 ++++---- .../api/{ => v1}/MeController.java | 0 .../{ => api/v1}/MerchantDebugController.java | 48 ++--- .../{ => api/v1}/ProductController.java | 110 ++++++------ .../{ => api/v1}/ProductV1Controller.java | 0 .../api/{ => v1}/StateController.java | 110 ++++++------ .../api/{ => v1}/UserController.java | 104 +++++------ .../api/{ => v1}/package-info.java | 0 .../AdminEnrichmentController.java | 0 .../{ => enums}/EnrichmentSource.java | 0 .../{ => enums}/EnrichmentStatus.java | 0 .../{ => enums}/EnrichmentType.java | 0 .../{ => model}/ProductEnrichment.java | 0 .../ProductEnrichmentRepository.java | 0 .../CaliberEnrichmentService.java | 0 34 files changed, 524 insertions(+), 516 deletions(-) create mode 100644 src/main/java/group/goforward/battlbuilder/catalog/query/package-info.java create mode 100644 src/main/java/group/goforward/battlbuilder/cli/package-info.java rename src/main/java/group/goforward/battlbuilder/{utils => common}/ApiResponse.java (96%) rename src/main/java/group/goforward/battlbuilder/{utils => common}/Counter.java (94%) rename src/main/java/group/goforward/battlbuilder/{utils => common}/package-info.java (100%) rename src/main/java/group/goforward/battlbuilder/{configuration => config}/CacheConfig.java (97%) rename src/main/java/group/goforward/battlbuilder/{configuration => config}/CorsConfig.java (97%) rename src/main/java/group/goforward/battlbuilder/{configuration => config}/JpaConfig.java (97%) rename src/main/java/group/goforward/battlbuilder/{configuration => config}/MinioConfig.java (100%) rename src/main/java/group/goforward/battlbuilder/{configuration => config}/PasswordConfig.java (100%) rename src/main/java/group/goforward/battlbuilder/{configuration => config}/SecurityConfig.java (100%) rename src/main/java/group/goforward/battlbuilder/{configuration => config}/package-info.java (100%) rename src/main/java/group/goforward/battlbuilder/controllers/{ => api/v1}/AuthController.java (100%) rename src/main/java/group/goforward/battlbuilder/controllers/api/{ => v1}/BrandController.java (97%) rename src/main/java/group/goforward/battlbuilder/controllers/api/{ => v1}/BuildController.java (96%) rename src/main/java/group/goforward/battlbuilder/controllers/{ => api/v1}/BuildV1Controller.java (100%) rename src/main/java/group/goforward/battlbuilder/controllers/{ => api/v1}/BuilderBootstrapController.java (100%) rename src/main/java/group/goforward/battlbuilder/controllers/{ => api/v1}/CatalogController.java (100%) rename src/main/java/group/goforward/battlbuilder/controllers/{ => api/v1}/CategoryController.java (100%) rename src/main/java/group/goforward/battlbuilder/controllers/{ => api/v1}/ImportController.java (97%) rename src/main/java/group/goforward/battlbuilder/controllers/api/{ => v1}/MeController.java (100%) rename src/main/java/group/goforward/battlbuilder/controllers/{ => api/v1}/MerchantDebugController.java (97%) rename src/main/java/group/goforward/battlbuilder/controllers/{ => api/v1}/ProductController.java (97%) rename src/main/java/group/goforward/battlbuilder/controllers/{ => api/v1}/ProductV1Controller.java (100%) rename src/main/java/group/goforward/battlbuilder/controllers/api/{ => v1}/StateController.java (97%) rename src/main/java/group/goforward/battlbuilder/controllers/api/{ => v1}/UserController.java (97%) rename src/main/java/group/goforward/battlbuilder/controllers/api/{ => v1}/package-info.java (100%) rename src/main/java/group/goforward/battlbuilder/enrichment/{ => controller}/AdminEnrichmentController.java (100%) rename src/main/java/group/goforward/battlbuilder/enrichment/{ => enums}/EnrichmentSource.java (100%) rename src/main/java/group/goforward/battlbuilder/enrichment/{ => enums}/EnrichmentStatus.java (100%) rename src/main/java/group/goforward/battlbuilder/enrichment/{ => enums}/EnrichmentType.java (100%) rename src/main/java/group/goforward/battlbuilder/enrichment/{ => model}/ProductEnrichment.java (100%) rename src/main/java/group/goforward/battlbuilder/enrichment/{ => repo}/ProductEnrichmentRepository.java (100%) rename src/main/java/group/goforward/battlbuilder/enrichment/{ => service}/CaliberEnrichmentService.java (100%) diff --git a/src/main/java/group/goforward/battlbuilder/catalog/query/package-info.java b/src/main/java/group/goforward/battlbuilder/catalog/query/package-info.java new file mode 100644 index 0000000..62afce8 --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/catalog/query/package-info.java @@ -0,0 +1,4 @@ +/** + * Query projections for the catalog domain. + */ +package group.goforward.battlbuilder.catalog.query; diff --git a/src/main/java/group/goforward/battlbuilder/cli/package-info.java b/src/main/java/group/goforward/battlbuilder/cli/package-info.java new file mode 100644 index 0000000..bbc8575 --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/cli/package-info.java @@ -0,0 +1,4 @@ +/** + * Command line runners and CLI utilities. + */ +package group.goforward.battlbuilder.cli; diff --git a/src/main/java/group/goforward/battlbuilder/utils/ApiResponse.java b/src/main/java/group/goforward/battlbuilder/common/ApiResponse.java similarity index 96% rename from src/main/java/group/goforward/battlbuilder/utils/ApiResponse.java rename to src/main/java/group/goforward/battlbuilder/common/ApiResponse.java index a97bcb6..1232466 100644 --- a/src/main/java/group/goforward/battlbuilder/utils/ApiResponse.java +++ b/src/main/java/group/goforward/battlbuilder/common/ApiResponse.java @@ -1,74 +1,74 @@ -package group.goforward.battlbuilder.utils; - -import java.time.LocalDateTime; - -/** - * @param - */ -public class ApiResponse { - - private static final String API_SUCCESS = "success"; - private static final String API_FAILURE = "failure"; - private static final String API_ERROR = "error"; - private String[] messages; - private T data; - - - - private String status; - private LocalDateTime timestamp; - private ApiResponse(String status, String[] message, T data) { - this.status = status; - this.messages = message; - this.data = data; - this.timestamp = LocalDateTime.now(); - } - - public static ApiResponse error(String message, T data) { - String[] msg = {message}; // ✅ Include the message - return new ApiResponse<>(API_ERROR, msg, data); - } - - public static ApiResponse success(T data, String emailSentSuccessfully) { - String[] msg = {}; - return new ApiResponse<>(API_SUCCESS, msg, data); - } - - public static ApiResponse error(String[] messages) { - return new ApiResponse<>(API_ERROR, messages, null); - } - public static ApiResponse error(String message) { - String[] msg = {}; - return new ApiResponse<>(API_ERROR, msg, null); - } - - - public String[] getMessages() { - return messages; - } - - public void setMessages(String[] messages) { - this.messages = messages; - } - - public T getData() {return data;} - public void setData(T data) { - this.data = data; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } - - public LocalDateTime getTimestamp() { - return timestamp; - } - - public void setTimestamp(LocalDateTime timestamp) { - this.timestamp = timestamp; - } -} +package group.goforward.battlbuilder.utils; + +import java.time.LocalDateTime; + +/** + * @param + */ +public class ApiResponse { + + private static final String API_SUCCESS = "success"; + private static final String API_FAILURE = "failure"; + private static final String API_ERROR = "error"; + private String[] messages; + private T data; + + + + private String status; + private LocalDateTime timestamp; + private ApiResponse(String status, String[] message, T data) { + this.status = status; + this.messages = message; + this.data = data; + this.timestamp = LocalDateTime.now(); + } + + public static ApiResponse error(String message, T data) { + String[] msg = {message}; // ✅ Include the message + return new ApiResponse<>(API_ERROR, msg, data); + } + + public static ApiResponse success(T data, String emailSentSuccessfully) { + String[] msg = {}; + return new ApiResponse<>(API_SUCCESS, msg, data); + } + + public static ApiResponse error(String[] messages) { + return new ApiResponse<>(API_ERROR, messages, null); + } + public static ApiResponse error(String message) { + String[] msg = {}; + return new ApiResponse<>(API_ERROR, msg, null); + } + + + public String[] getMessages() { + return messages; + } + + public void setMessages(String[] messages) { + this.messages = messages; + } + + public T getData() {return data;} + public void setData(T data) { + this.data = data; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } +} diff --git a/src/main/java/group/goforward/battlbuilder/utils/Counter.java b/src/main/java/group/goforward/battlbuilder/common/Counter.java similarity index 94% rename from src/main/java/group/goforward/battlbuilder/utils/Counter.java rename to src/main/java/group/goforward/battlbuilder/common/Counter.java index 6836ed8..e5b85d5 100644 --- a/src/main/java/group/goforward/battlbuilder/utils/Counter.java +++ b/src/main/java/group/goforward/battlbuilder/common/Counter.java @@ -1,20 +1,20 @@ -package group.goforward.battlbuilder.utils; - -import org.springframework.context.annotation.Bean; - -public class Counter { - - Integer count = 0; - public void addOne() { - count +=1; - } - - public Integer getCount() { - return count; - } - - private void setCount(Integer count) { - this.count = count; - } - -} +package group.goforward.battlbuilder.utils; + +import org.springframework.context.annotation.Bean; + +public class Counter { + + Integer count = 0; + public void addOne() { + count +=1; + } + + public Integer getCount() { + return count; + } + + private void setCount(Integer count) { + this.count = count; + } + +} diff --git a/src/main/java/group/goforward/battlbuilder/utils/package-info.java b/src/main/java/group/goforward/battlbuilder/common/package-info.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/utils/package-info.java rename to src/main/java/group/goforward/battlbuilder/common/package-info.java diff --git a/src/main/java/group/goforward/battlbuilder/configuration/CacheConfig.java b/src/main/java/group/goforward/battlbuilder/config/CacheConfig.java similarity index 97% rename from src/main/java/group/goforward/battlbuilder/configuration/CacheConfig.java rename to src/main/java/group/goforward/battlbuilder/config/CacheConfig.java index 770c0ef..c921be2 100644 --- a/src/main/java/group/goforward/battlbuilder/configuration/CacheConfig.java +++ b/src/main/java/group/goforward/battlbuilder/config/CacheConfig.java @@ -1,22 +1,22 @@ -package group.goforward.battlbuilder.configuration; - -import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.cache.concurrent.ConcurrentMapCacheManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableCaching -public class CacheConfig { - - @Bean - public CacheManager cacheManager() { - // Must match the @Cacheable value(s) used in controllers/services. - // ProductV1Controller uses: "gunbuilderProductsV1" - return new ConcurrentMapCacheManager( - "gunbuilderProductsV1", - "gunbuilderProducts" // keep if anything else still references it - ); - } +package group.goforward.battlbuilder.configuration; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + // Must match the @Cacheable value(s) used in controllers/services. + // ProductV1Controller uses: "gunbuilderProductsV1" + return new ConcurrentMapCacheManager( + "gunbuilderProductsV1", + "gunbuilderProducts" // keep if anything else still references it + ); + } } \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/configuration/CorsConfig.java b/src/main/java/group/goforward/battlbuilder/config/CorsConfig.java similarity index 97% rename from src/main/java/group/goforward/battlbuilder/configuration/CorsConfig.java rename to src/main/java/group/goforward/battlbuilder/config/CorsConfig.java index 20ff722..26ebe73 100644 --- a/src/main/java/group/goforward/battlbuilder/configuration/CorsConfig.java +++ b/src/main/java/group/goforward/battlbuilder/config/CorsConfig.java @@ -1,84 +1,84 @@ -// src/main/java/com/example/config/CorsConfig.java -package group.goforward.battlbuilder.configuration; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.filter.CorsFilter; - -import java.util.Arrays; - -@Configuration -public class CorsConfig { - - @Bean - public CorsFilter corsFilter() { - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - CorsConfiguration config = new CorsConfiguration(); - - // Allow credentials - config.setAllowCredentials(true); - - // Allow Angular development server - config.setAllowedOrigins(Arrays.asList( - "http://localhost:4200", - "http://localhost:4201", - "http://localhost:8070", - "https://localhost:8070", - "http://localhost:8080", - "https://localhost:8080", - "http://localhost:3000", - "https://localhost:3000", - "https://localhost:3000/gunbuilder", - "http://localhost:3000/gunbuilder", - "https://localhost:3000/builder", - "http://localhost:3000/builder" - )); - - // Allow all headers - config.addAllowedHeader("*"); - - // Allow all HTTP methods - config.setAllowedMethods(Arrays.asList( - "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS" - )); - - // Expose headers - config.setExposedHeaders(Arrays.asList( - "Authorization", - "Content-Type", - "X-Total-Count" - )); - - // Max age for preflight cache (1 hour) - config.setMaxAge(3600L); - - source.registerCorsConfiguration("/**", config); - return new CorsFilter(source); - } -} - - -// Alternative using WebMvcConfigurer: -/* -package group.goforward.citysites.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class WebConfig implements WebMvcConfigurer { - - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/api/**") - .allowedOrigins("http://localhost:4200") - .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true) - .maxAge(3600); - } -} +// src/main/java/com/example/config/CorsConfig.java +package group.goforward.battlbuilder.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Arrays; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + + // Allow credentials + config.setAllowCredentials(true); + + // Allow Angular development server + config.setAllowedOrigins(Arrays.asList( + "http://localhost:4200", + "http://localhost:4201", + "http://localhost:8070", + "https://localhost:8070", + "http://localhost:8080", + "https://localhost:8080", + "http://localhost:3000", + "https://localhost:3000", + "https://localhost:3000/gunbuilder", + "http://localhost:3000/gunbuilder", + "https://localhost:3000/builder", + "http://localhost:3000/builder" + )); + + // Allow all headers + config.addAllowedHeader("*"); + + // Allow all HTTP methods + config.setAllowedMethods(Arrays.asList( + "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS" + )); + + // Expose headers + config.setExposedHeaders(Arrays.asList( + "Authorization", + "Content-Type", + "X-Total-Count" + )); + + // Max age for preflight cache (1 hour) + config.setMaxAge(3600L); + + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} + + +// Alternative using WebMvcConfigurer: +/* +package group.goforward.citysites.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/api/**") + .allowedOrigins("http://localhost:4200") + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } +} */ \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/configuration/JpaConfig.java b/src/main/java/group/goforward/battlbuilder/config/JpaConfig.java similarity index 97% rename from src/main/java/group/goforward/battlbuilder/configuration/JpaConfig.java rename to src/main/java/group/goforward/battlbuilder/config/JpaConfig.java index eca843f..09e6ce4 100644 --- a/src/main/java/group/goforward/battlbuilder/configuration/JpaConfig.java +++ b/src/main/java/group/goforward/battlbuilder/config/JpaConfig.java @@ -1,10 +1,10 @@ -package group.goforward.battlbuilder.configuration; - -import org.springframework.context.annotation.Configuration; -//import org.springframework.data.jpa.repository.config.EnableJpaAuditing; - -@Configuration - -public class JpaConfig { - // Enables @CreatedDate / @LastModifiedDate processing +package group.goforward.battlbuilder.configuration; + +import org.springframework.context.annotation.Configuration; +//import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration + +public class JpaConfig { + // Enables @CreatedDate / @LastModifiedDate processing } \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/configuration/MinioConfig.java b/src/main/java/group/goforward/battlbuilder/config/MinioConfig.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/configuration/MinioConfig.java rename to src/main/java/group/goforward/battlbuilder/config/MinioConfig.java diff --git a/src/main/java/group/goforward/battlbuilder/configuration/PasswordConfig.java b/src/main/java/group/goforward/battlbuilder/config/PasswordConfig.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/configuration/PasswordConfig.java rename to src/main/java/group/goforward/battlbuilder/config/PasswordConfig.java diff --git a/src/main/java/group/goforward/battlbuilder/configuration/SecurityConfig.java b/src/main/java/group/goforward/battlbuilder/config/SecurityConfig.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/configuration/SecurityConfig.java rename to src/main/java/group/goforward/battlbuilder/config/SecurityConfig.java diff --git a/src/main/java/group/goforward/battlbuilder/configuration/package-info.java b/src/main/java/group/goforward/battlbuilder/config/package-info.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/configuration/package-info.java rename to src/main/java/group/goforward/battlbuilder/config/package-info.java diff --git a/src/main/java/group/goforward/battlbuilder/controllers/AuthController.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/AuthController.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/controllers/AuthController.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/AuthController.java diff --git a/src/main/java/group/goforward/battlbuilder/controllers/api/BrandController.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/BrandController.java similarity index 97% rename from src/main/java/group/goforward/battlbuilder/controllers/api/BrandController.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/BrandController.java index f5ec691..78b4795 100644 --- a/src/main/java/group/goforward/battlbuilder/controllers/api/BrandController.java +++ b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/BrandController.java @@ -1,50 +1,50 @@ -package group.goforward.battlbuilder.controllers; - -import group.goforward.battlbuilder.model.Brand; -import group.goforward.battlbuilder.repos.BrandRepository; -import group.goforward.battlbuilder.services.BrandService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - - -@RestController -@RequestMapping({"/api/v1/brands", "/api/brands"}) -public class BrandController { - @Autowired - private BrandRepository repo; - @Autowired - private BrandService brandService; -//@Cacheable(value="getAllStates") - @GetMapping("/all") - public ResponseEntity> getAllBrands() { - List brand = repo.findAll(); - return ResponseEntity.ok(brand); - } - - @GetMapping("/{id}") - public ResponseEntity getAllBrandsById(@PathVariable Integer id) { - return repo.findById(id) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } - - @PostMapping("/add") - public ResponseEntity createbrand(@RequestBody Brand item) { - Brand created = brandService.save(item); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - - @DeleteMapping("/delete/{id}") - public ResponseEntity deleteItem(@PathVariable Integer id) { - return brandService.findById(id) - .map(item -> { - brandService.deleteById(id); - return ResponseEntity.noContent().build(); - }) - .orElse(ResponseEntity.notFound().build()); - } -} +package group.goforward.battlbuilder.controllers; + +import group.goforward.battlbuilder.model.Brand; +import group.goforward.battlbuilder.repos.BrandRepository; +import group.goforward.battlbuilder.services.BrandService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@RestController +@RequestMapping({"/api/v1/brands", "/api/brands"}) +public class BrandController { + @Autowired + private BrandRepository repo; + @Autowired + private BrandService brandService; +//@Cacheable(value="getAllStates") + @GetMapping("/all") + public ResponseEntity> getAllBrands() { + List brand = repo.findAll(); + return ResponseEntity.ok(brand); + } + + @GetMapping("/{id}") + public ResponseEntity getAllBrandsById(@PathVariable Integer id) { + return repo.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping("/add") + public ResponseEntity createbrand(@RequestBody Brand item) { + Brand created = brandService.save(item); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + + @DeleteMapping("/delete/{id}") + public ResponseEntity deleteItem(@PathVariable Integer id) { + return brandService.findById(id) + .map(item -> { + brandService.deleteById(id); + return ResponseEntity.noContent().build(); + }) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/src/main/java/group/goforward/battlbuilder/controllers/api/BuildController.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/BuildController.java similarity index 96% rename from src/main/java/group/goforward/battlbuilder/controllers/api/BuildController.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/BuildController.java index 11b3f0b..7beca38 100644 --- a/src/main/java/group/goforward/battlbuilder/controllers/api/BuildController.java +++ b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/BuildController.java @@ -1,34 +1,34 @@ -package group.goforward.battlbuilder.controllers.api; - -import group.goforward.battlbuilder.model.Build; -import group.goforward.battlbuilder.repos.BuildRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - - -@RestController -@RequestMapping("/v1/api/builds") -public class BuildController { - @Autowired - private BuildRepository repo; - @Autowired - // private BuildsService service; -//@Cacheable(value="getAllStates") - @GetMapping("/all") - public ResponseEntity> getAll() { - List builds = repo.findAll(); - return ResponseEntity.ok(builds); - } - - @GetMapping("/{id}") - public ResponseEntity getAllBuildsById(@PathVariable Integer id) { - return repo.findById(id) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } - - -} +package group.goforward.battlbuilder.controllers.api; + +import group.goforward.battlbuilder.model.Build; +import group.goforward.battlbuilder.repos.BuildRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@RestController +@RequestMapping("/v1/api/builds") +public class BuildController { + @Autowired + private BuildRepository repo; + @Autowired + // private BuildsService service; +//@Cacheable(value="getAllStates") + @GetMapping("/all") + public ResponseEntity> getAll() { + List builds = repo.findAll(); + return ResponseEntity.ok(builds); + } + + @GetMapping("/{id}") + public ResponseEntity getAllBuildsById(@PathVariable Integer id) { + return repo.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + +} diff --git a/src/main/java/group/goforward/battlbuilder/controllers/BuildV1Controller.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/BuildV1Controller.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/controllers/BuildV1Controller.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/BuildV1Controller.java diff --git a/src/main/java/group/goforward/battlbuilder/controllers/BuilderBootstrapController.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/BuilderBootstrapController.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/controllers/BuilderBootstrapController.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/BuilderBootstrapController.java diff --git a/src/main/java/group/goforward/battlbuilder/controllers/CatalogController.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/CatalogController.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/controllers/CatalogController.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/CatalogController.java diff --git a/src/main/java/group/goforward/battlbuilder/controllers/CategoryController.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/CategoryController.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/controllers/CategoryController.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/CategoryController.java diff --git a/src/main/java/group/goforward/battlbuilder/controllers/ImportController.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/ImportController.java similarity index 97% rename from src/main/java/group/goforward/battlbuilder/controllers/ImportController.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/ImportController.java index 8dc878e..12e4759 100644 --- a/src/main/java/group/goforward/battlbuilder/controllers/ImportController.java +++ b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/ImportController.java @@ -1,40 +1,40 @@ -package group.goforward.battlbuilder.controllers; - -import group.goforward.battlbuilder.services.MerchantFeedImportService; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping({"/api/admin/imports", "/api/v1/admin/imports"}) - -@CrossOrigin(origins = "http://localhost:3000") -public class ImportController { - - private final MerchantFeedImportService merchantFeedImportService; - - public ImportController(MerchantFeedImportService merchantFeedImportService) { - this.merchantFeedImportService = merchantFeedImportService; - } - - /** - * Full product + offer import for a merchant. - * - * POST /admin/imports/{merchantId} - */ - @PostMapping("/{merchantId}") - public ResponseEntity importMerchant(@PathVariable Integer merchantId) { - merchantFeedImportService.importMerchantFeed(merchantId); - return ResponseEntity.noContent().build(); - } - - /** - * Offers-only sync (price/stock) for a merchant. - * - * POST /admin/imports/{merchantId}/offers-only - */ - @PostMapping("/{merchantId}/offers-only") - public ResponseEntity syncOffersOnly(@PathVariable Integer merchantId) { - merchantFeedImportService.syncOffersOnly(merchantId); - return ResponseEntity.noContent().build(); - } +package group.goforward.battlbuilder.controllers; + +import group.goforward.battlbuilder.services.MerchantFeedImportService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping({"/api/admin/imports", "/api/v1/admin/imports"}) + +@CrossOrigin(origins = "http://localhost:3000") +public class ImportController { + + private final MerchantFeedImportService merchantFeedImportService; + + public ImportController(MerchantFeedImportService merchantFeedImportService) { + this.merchantFeedImportService = merchantFeedImportService; + } + + /** + * Full product + offer import for a merchant. + * + * POST /admin/imports/{merchantId} + */ + @PostMapping("/{merchantId}") + public ResponseEntity importMerchant(@PathVariable Integer merchantId) { + merchantFeedImportService.importMerchantFeed(merchantId); + return ResponseEntity.noContent().build(); + } + + /** + * Offers-only sync (price/stock) for a merchant. + * + * POST /admin/imports/{merchantId}/offers-only + */ + @PostMapping("/{merchantId}/offers-only") + public ResponseEntity syncOffersOnly(@PathVariable Integer merchantId) { + merchantFeedImportService.syncOffersOnly(merchantId); + return ResponseEntity.noContent().build(); + } } \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/controllers/api/MeController.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/MeController.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/controllers/api/MeController.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/MeController.java diff --git a/src/main/java/group/goforward/battlbuilder/controllers/MerchantDebugController.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/MerchantDebugController.java similarity index 97% rename from src/main/java/group/goforward/battlbuilder/controllers/MerchantDebugController.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/MerchantDebugController.java index 9461e48..af65d42 100644 --- a/src/main/java/group/goforward/battlbuilder/controllers/MerchantDebugController.java +++ b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/MerchantDebugController.java @@ -1,25 +1,25 @@ -package group.goforward.battlbuilder.controllers; - -import group.goforward.battlbuilder.model.Merchant; -import group.goforward.battlbuilder.repos.MerchantRepository; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -@RestController -@RequestMapping({"/api/admin", "/api/v1/admin"}) -public class MerchantDebugController { - - private final MerchantRepository merchantRepository; - - public MerchantDebugController(MerchantRepository merchantRepository) { - this.merchantRepository = merchantRepository; - } - - @GetMapping("/debug/merchants") - public List listMerchants() { - return merchantRepository.findAll(); - } +package group.goforward.battlbuilder.controllers; + +import group.goforward.battlbuilder.model.Merchant; +import group.goforward.battlbuilder.repos.MerchantRepository; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping({"/api/admin", "/api/v1/admin"}) +public class MerchantDebugController { + + private final MerchantRepository merchantRepository; + + public MerchantDebugController(MerchantRepository merchantRepository) { + this.merchantRepository = merchantRepository; + } + + @GetMapping("/debug/merchants") + public List listMerchants() { + return merchantRepository.findAll(); + } } \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/controllers/ProductController.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/ProductController.java similarity index 97% rename from src/main/java/group/goforward/battlbuilder/controllers/ProductController.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/ProductController.java index de83357..64e0384 100644 --- a/src/main/java/group/goforward/battlbuilder/controllers/ProductController.java +++ b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/ProductController.java @@ -1,56 +1,56 @@ -package group.goforward.battlbuilder.controllers; - -import group.goforward.battlbuilder.web.dto.ProductOfferDto; -import group.goforward.battlbuilder.web.dto.ProductSummaryDto; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -/** - * LEGACY CONTROLLER (Deprecated) - * - * Do not add new features here. - * Canonical API lives in ProductV1Controller (/api/v1/products). - * - * This exists only to keep older clients working temporarily. - * Disable by default using: - * app.api.legacy.enabled=false - * - * NOTE: - * Even when disabled, Spring still compiles this class. So it must not reference - * missing services/methods. - */ -@Deprecated -@RestController -@RequestMapping({"/api/products", "/api/v1/products"}) -@CrossOrigin -@ConditionalOnProperty(name = "app.api.legacy.enabled", havingValue = "true", matchIfMissing = false) -public class ProductController { - - private static final String MSG = - "Legacy endpoint disabled. Use /api/v1/products instead."; - - @GetMapping - public ResponseEntity getProducts( - @RequestParam(defaultValue = "AR-15") String platform, - @RequestParam(required = false, name = "partRoles") List partRoles - ) { - // Legacy disabled by design (Option B cleanup) - return ResponseEntity.status(410).body(MSG); - } - - @GetMapping("/{id}/offers") - public ResponseEntity getOffersForProduct(@PathVariable("id") Integer productId) { - return ResponseEntity.status(410).body(MSG); - } - - @GetMapping("/{id}") - public ResponseEntity getProductById(@PathVariable("id") Integer productId) { - return ResponseEntity.status(410).body(MSG); - } - - // If you *really* need typed responses for an old client, we can re-add - // a real service layer once we align on the actual ProductQueryService API. +package group.goforward.battlbuilder.controllers; + +import group.goforward.battlbuilder.web.dto.ProductOfferDto; +import group.goforward.battlbuilder.web.dto.ProductSummaryDto; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * LEGACY CONTROLLER (Deprecated) + * + * Do not add new features here. + * Canonical API lives in ProductV1Controller (/api/v1/products). + * + * This exists only to keep older clients working temporarily. + * Disable by default using: + * app.api.legacy.enabled=false + * + * NOTE: + * Even when disabled, Spring still compiles this class. So it must not reference + * missing services/methods. + */ +@Deprecated +@RestController +@RequestMapping({"/api/products", "/api/v1/products"}) +@CrossOrigin +@ConditionalOnProperty(name = "app.api.legacy.enabled", havingValue = "true", matchIfMissing = false) +public class ProductController { + + private static final String MSG = + "Legacy endpoint disabled. Use /api/v1/products instead."; + + @GetMapping + public ResponseEntity getProducts( + @RequestParam(defaultValue = "AR-15") String platform, + @RequestParam(required = false, name = "partRoles") List partRoles + ) { + // Legacy disabled by design (Option B cleanup) + return ResponseEntity.status(410).body(MSG); + } + + @GetMapping("/{id}/offers") + public ResponseEntity getOffersForProduct(@PathVariable("id") Integer productId) { + return ResponseEntity.status(410).body(MSG); + } + + @GetMapping("/{id}") + public ResponseEntity getProductById(@PathVariable("id") Integer productId) { + return ResponseEntity.status(410).body(MSG); + } + + // If you *really* need typed responses for an old client, we can re-add + // a real service layer once we align on the actual ProductQueryService API. } \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/controllers/ProductV1Controller.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/ProductV1Controller.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/controllers/ProductV1Controller.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/ProductV1Controller.java diff --git a/src/main/java/group/goforward/battlbuilder/controllers/api/StateController.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/StateController.java similarity index 97% rename from src/main/java/group/goforward/battlbuilder/controllers/api/StateController.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/StateController.java index 3d8a6ef..60c66f9 100644 --- a/src/main/java/group/goforward/battlbuilder/controllers/api/StateController.java +++ b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/StateController.java @@ -1,55 +1,55 @@ -package group.goforward.battlbuilder.controllers; - -import group.goforward.battlbuilder.model.State; -import group.goforward.battlbuilder.repos.StateRepository; -import group.goforward.battlbuilder.services.admin.StatesService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - - -@RestController -@RequestMapping({"/api/states", "/api/v1/states"}) -public class StateController { - @Autowired - private StateRepository repo; - @Autowired - private StatesService statesService; -//@Cacheable(value="getAllStates") - @GetMapping("/all") - public ResponseEntity> getAllStates() { - List state = repo.findAll(); - return ResponseEntity.ok(state); - } - - @GetMapping("/{id}") - public ResponseEntity getAllStatesById(@PathVariable Integer id) { - return repo.findById(id) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } - @GetMapping("/byAbbrev/{abbreviation}") - public ResponseEntity getAllStatesByAbbreviation(@PathVariable String abbreviation) { - return repo.findByAbbreviation(abbreviation) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } - @PostMapping("/addState") - public ResponseEntity createState(@RequestBody State item) { - State created = statesService.save(item); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - - @DeleteMapping("/deleteState/{id}") - public ResponseEntity deleteItem(@PathVariable Integer id) { - return statesService.findById(id) - .map(item -> { - statesService.deleteById(id); - return ResponseEntity.noContent().build(); - }) - .orElse(ResponseEntity.notFound().build()); - } -} +package group.goforward.battlbuilder.controllers; + +import group.goforward.battlbuilder.model.State; +import group.goforward.battlbuilder.repos.StateRepository; +import group.goforward.battlbuilder.services.admin.StatesService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@RestController +@RequestMapping({"/api/states", "/api/v1/states"}) +public class StateController { + @Autowired + private StateRepository repo; + @Autowired + private StatesService statesService; +//@Cacheable(value="getAllStates") + @GetMapping("/all") + public ResponseEntity> getAllStates() { + List state = repo.findAll(); + return ResponseEntity.ok(state); + } + + @GetMapping("/{id}") + public ResponseEntity getAllStatesById(@PathVariable Integer id) { + return repo.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + @GetMapping("/byAbbrev/{abbreviation}") + public ResponseEntity getAllStatesByAbbreviation(@PathVariable String abbreviation) { + return repo.findByAbbreviation(abbreviation) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + @PostMapping("/addState") + public ResponseEntity createState(@RequestBody State item) { + State created = statesService.save(item); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + + @DeleteMapping("/deleteState/{id}") + public ResponseEntity deleteItem(@PathVariable Integer id) { + return statesService.findById(id) + .map(item -> { + statesService.deleteById(id); + return ResponseEntity.noContent().build(); + }) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/src/main/java/group/goforward/battlbuilder/controllers/api/UserController.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/UserController.java similarity index 97% rename from src/main/java/group/goforward/battlbuilder/controllers/api/UserController.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/UserController.java index 6088209..e8932df 100644 --- a/src/main/java/group/goforward/battlbuilder/controllers/api/UserController.java +++ b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/UserController.java @@ -1,52 +1,52 @@ -package group.goforward.battlbuilder.controllers; - -import group.goforward.battlbuilder.model.User; -import group.goforward.battlbuilder.repos.UserRepository; -import group.goforward.battlbuilder.services.admin.UsersService; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - - -@RestController -@RequestMapping({"/api/user", "/api/v1/user"}) -public class UserController { - private final UserRepository repo; - private final UsersService usersService; - - public UserController(UserRepository repo, UsersService usersService) { - this.repo = repo; - this.usersService = usersService; - } - - @GetMapping("/all") - public ResponseEntity> getAllUsers() { - List data = repo.findAll(); - return ResponseEntity.ok(data); - } - - - @GetMapping("/byId/{id}") - public ResponseEntity getAllStatesById(@PathVariable Integer id) { - return repo.findById(id) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } - @PostMapping("/addUser") - public ResponseEntity createUser(@RequestBody User item) { - User created = usersService.save(item); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - - @DeleteMapping("/deleteUser/{id}") - public ResponseEntity deleteItem(@PathVariable Integer id) { - return usersService.findById(id) - .map(item -> { - usersService.deleteById(id); - return ResponseEntity.noContent().build(); - }) - .orElse(ResponseEntity.notFound().build()); - } -} +package group.goforward.battlbuilder.controllers; + +import group.goforward.battlbuilder.model.User; +import group.goforward.battlbuilder.repos.UserRepository; +import group.goforward.battlbuilder.services.admin.UsersService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@RestController +@RequestMapping({"/api/user", "/api/v1/user"}) +public class UserController { + private final UserRepository repo; + private final UsersService usersService; + + public UserController(UserRepository repo, UsersService usersService) { + this.repo = repo; + this.usersService = usersService; + } + + @GetMapping("/all") + public ResponseEntity> getAllUsers() { + List data = repo.findAll(); + return ResponseEntity.ok(data); + } + + + @GetMapping("/byId/{id}") + public ResponseEntity getAllStatesById(@PathVariable Integer id) { + return repo.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + @PostMapping("/addUser") + public ResponseEntity createUser(@RequestBody User item) { + User created = usersService.save(item); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + + @DeleteMapping("/deleteUser/{id}") + public ResponseEntity deleteItem(@PathVariable Integer id) { + return usersService.findById(id) + .map(item -> { + usersService.deleteById(id); + return ResponseEntity.noContent().build(); + }) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/src/main/java/group/goforward/battlbuilder/controllers/api/package-info.java b/src/main/java/group/goforward/battlbuilder/controllers/api/v1/package-info.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/controllers/api/package-info.java rename to src/main/java/group/goforward/battlbuilder/controllers/api/v1/package-info.java diff --git a/src/main/java/group/goforward/battlbuilder/enrichment/AdminEnrichmentController.java b/src/main/java/group/goforward/battlbuilder/enrichment/controller/AdminEnrichmentController.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/enrichment/AdminEnrichmentController.java rename to src/main/java/group/goforward/battlbuilder/enrichment/controller/AdminEnrichmentController.java diff --git a/src/main/java/group/goforward/battlbuilder/enrichment/EnrichmentSource.java b/src/main/java/group/goforward/battlbuilder/enrichment/enums/EnrichmentSource.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/enrichment/EnrichmentSource.java rename to src/main/java/group/goforward/battlbuilder/enrichment/enums/EnrichmentSource.java diff --git a/src/main/java/group/goforward/battlbuilder/enrichment/EnrichmentStatus.java b/src/main/java/group/goforward/battlbuilder/enrichment/enums/EnrichmentStatus.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/enrichment/EnrichmentStatus.java rename to src/main/java/group/goforward/battlbuilder/enrichment/enums/EnrichmentStatus.java diff --git a/src/main/java/group/goforward/battlbuilder/enrichment/EnrichmentType.java b/src/main/java/group/goforward/battlbuilder/enrichment/enums/EnrichmentType.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/enrichment/EnrichmentType.java rename to src/main/java/group/goforward/battlbuilder/enrichment/enums/EnrichmentType.java diff --git a/src/main/java/group/goforward/battlbuilder/enrichment/ProductEnrichment.java b/src/main/java/group/goforward/battlbuilder/enrichment/model/ProductEnrichment.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/enrichment/ProductEnrichment.java rename to src/main/java/group/goforward/battlbuilder/enrichment/model/ProductEnrichment.java diff --git a/src/main/java/group/goforward/battlbuilder/enrichment/ProductEnrichmentRepository.java b/src/main/java/group/goforward/battlbuilder/enrichment/repo/ProductEnrichmentRepository.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/enrichment/ProductEnrichmentRepository.java rename to src/main/java/group/goforward/battlbuilder/enrichment/repo/ProductEnrichmentRepository.java diff --git a/src/main/java/group/goforward/battlbuilder/enrichment/CaliberEnrichmentService.java b/src/main/java/group/goforward/battlbuilder/enrichment/service/CaliberEnrichmentService.java similarity index 100% rename from src/main/java/group/goforward/battlbuilder/enrichment/CaliberEnrichmentService.java rename to src/main/java/group/goforward/battlbuilder/enrichment/service/CaliberEnrichmentService.java