39 Commits

Author SHA1 Message Date
90b36c7933 fixed the CORS issue 2025-12-05 23:33:18 -05:00
290ef6cea4 removing jpa stuff from old project 2025-12-05 21:57:21 -05:00
12d2ee53b5 Merge pull request 'seanr, not sure what this will do' (#1) from sean into master
Reviewed-on: https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring/pulls/1
2025-12-04 15:29:09 -05:00
2969cdfa23 Merge remote-tracking branch 'origin/dont-lose-this' into sean 2025-12-04 15:22:36 -05:00
e986fa97ca running finally.. 2025-12-04 15:20:42 -05:00
c283ec15b6 Merge remote-tracking branch 'origin/dont-lose-this' into sean 2025-12-04 15:15:04 -05:00
9096ddd165 running finally.. 2025-12-04 15:06:29 -05:00
3d1501cc87 running finally.. 2025-12-04 14:43:07 -05:00
3ae68f30c0 added more namedqueries 2025-12-04 12:39:02 -05:00
f3626af709 sharing database connectivity file 2025-12-04 10:07:04 -05:00
756a6791fc fixed merge conflict 2025-12-03 22:12:46 -05:00
d344b372d1 fixing api endpoints and added brands controllers, repo 2025-12-03 22:17:46 -05:00
74a5c42e26 dynamic category mapping and updating from admin. 2025-12-03 21:50:00 -05:00
5e3f7d5044 expanded category grouping from db 2025-12-03 19:13:43 -05:00
31815d3145 docker-compose fixes 2025-12-03 11:34:08 -05:00
4138edf45d lots of changes that I don't think I made, must of been the pull 2025-12-03 11:22:50 -05:00
85b00e9d99 jaca security auth - working 2025-12-03 10:55:35 -05:00
7e1b33efdf added java caching and optimized controller queries 2025-12-02 20:19:56 -05:00
009e512a66 docker... 2025-12-02 17:23:32 -05:00
9fabf30406 getting ready for docker deployment 2025-12-02 17:18:26 -05:00
346ccc3813 got ride of the swagger geneerated api end oints 2025-12-02 14:42:34 -05:00
9779bdb5c0 package-infos and a few other changes 2025-12-02 13:55:01 -05:00
d7ae362c23 readme docs 2025-12-02 07:21:23 -05:00
7fb24fdde3 buffer 2025-12-02 05:41:17 -05:00
c4d2adad1a running build. New merchant import admin page. 2025-12-01 21:28:39 -05:00
0b2b3afd0c adding more package-info dfiles 2025-12-01 16:58:49 -05:00
f1dcd10a79 new categories and mapping logic 2025-12-01 16:35:29 -05:00
66d45a1113 adding package-info files and move a controller to the package with tthe res of them 2025-12-01 15:41:55 -05:00
0f5978fd11 allow for category mapping from GB UI. Add platform locked flag to products. 2025-12-01 07:54:05 -05:00
7166b92d32 some tweaks for product offers. 2025-11-30 21:08:29 -05:00
855f1c23c9 90ish percent working. data flows, offers are create and pricings is pulling. 2025-11-30 10:27:38 -05:00
2ef96939f4 category mapping work. app runs still :D 2025-11-30 08:08:17 -05:00
52c49c7238 no idea? 2025-11-30 06:39:07 -05:00
140b75621f running import from csv off Avant. Needs some clean up on category matching. 2025-11-30 06:38:51 -05:00
f539c64588 small changes. still working 2025-11-30 05:40:59 -05:00
87b3c4bff8 slug handling changes 2025-11-30 05:22:08 -05:00
4c0a3bd12d moving controllers to controllers package 2025-11-29 23:18:04 -05:00
5407c245eb working pipeline. curl admin/imports/4 imported a test product into the db. 2025-11-29 10:55:56 -05:00
0baae3bc6c project compiles 2025-11-29 10:18:54 -05:00
133 changed files with 57624 additions and 3781 deletions

31
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="r710" uuid="e6a29f5c-71d9-45f0-931b-554bcf8a94ba">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://r710.dev.gofwd.group:5433/postgres</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="ss_builder@r710.gofwd.group" uuid="e0fa459b-2f6c-45f1-9c41-66423c870df9">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/src/main/resources/application.properties</remarks>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://r710.gofwd.group:5433/ss_builder</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -0,0 +1,67 @@
# Ballistic Builder ( The Armory?) Backend
### Internal Engine for the Shadow System Armory?
The Ballistic Backend is the operational backbone behind the Shadown System Armory and its admin tools. It ingests merchant feeds, normalizes product data, manages categories, synchronizes prices, and powers the compatibility, pricing, and product logic behind the consumer-facing Builder.
Its built for reliability, longevity, and clean extensibility — the kind of foundation you want when scaling from a small beta to a fully public platform.
---
## What This Backend Does
### **Merchant Feed Ingestion**
- Pulls AvantLink feeds (CSV or TSV)
- Automatically detects delimiters
- Normalizes raw merchant fields
- Creates or updates product records
- Upserts price and stock offers
- Tracks first-seen / last-seen timestamps
- Safely handles malformed or incomplete rows
- Ensures repeat imports never duplicate offers
### **Category Mapping Engine**
- Identifies every unique raw category coming from each merchant feed
- Exposes *unmapped* categories in the admin UI
- Allows you to assign:
- Part Role
- Product Configuration (Stripped, Complete, Kit, etc.)
- Applies mappings automatically on future imports
- Respects manual overrides such as `platform_locked`
### **Builder Support**
The frontend Builder depends on this backend for:
- Loading parts grouped by role
- Offering compatible options
- Calculating build cost
- Comparing offers across merchants
- Providing product metadata, imagery, and offer data
**Future expansions include:** price history, compatibility engine, build exports, public build pages, multi-merchant aggregation, and automated anomaly detection.
---
## Tech Stack
- **Spring Boot 3.x**
- **Java 17**
- **PostgreSQL**
- **Hibernate (JPA)**
- **HikariCP**
- **Apache Commons CSV**
- **Maven**
- **REST API**
---
## Local Development
### Requirements
- Java 17 or newer
- PostgreSQL running locally
- Port 8080 open (default backend port)
### Run Development Server
```bash
./mvnw spring-boot:run

39
action1.yaml Normal file
View File

@@ -0,0 +1,39 @@
# File: .gitea/workflows/build-and-upload.yml
name: Build and Upload Artifact
on:
push:
branches:
- main
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
# Step 1: Check out repository code
- name: Checkout code
uses: actions/checkout@v4
# Step 2: Set up Node.js (example for a JS project; adjust for your stack)
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
# Step 3: Install dependencies
- name: Install dependencies
run: npm ci
# Step 4: Build project
- name: Build project
run: npm run build
# Step 5: Upload build output as artifact
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/ # Change to your build output directory
retention-days: 7 # Optional: how long to keep artifact

34
docker/backend/Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# Stage 1: Build the application (The Build Stage)
# Use a Java SDK image with Maven pre-installed
FROM maven:3.9-jdk-17-slim AS build
# Set the working directory inside the container
WORKDIR /app
# Copy the Maven project files (pom.xml) first to leverage Docker layer caching
COPY pom.xml .
# Copy the source code
COPY src ./src
# Build the Spring Boot application, skipping tests to speed up the Docker build
# This creates the executable JAR file in the 'target' directory
RUN mvn clean package -DskipTests
# Stage 2: Create the final lightweight image (The Runtime Stage)
# Use a smaller Java Runtime Environment (JRE) image for a smaller footprint
FROM openjdk:17-jre-slim
# Set the working directory in the final image
WORKDIR /app
# Copy the built JAR file from the 'build' stage into the final image
# The JAR file is typically named 'target/<your-app-name>-<version>.jar'
# You may need to adjust the name if you have a non-standard pom.xml
COPY --from=build /app/target/*.jar app.jar
# Expose the default Spring Boot port
EXPOSE 8080
# Define the command to run the application
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -0,0 +1,43 @@
version: '3.8'
services:
# --- 1. Spring API Service (Backend) ---
ss_builder-api:
build:
context: ./backend # Path to your Spring project's root folder
dockerfile: Dockerfile # Assumes you have a Dockerfile in ./backend
container_name: ss_builder-api
ports:
- "8080:8080" # Map host port 8080 to container port 8080
environment:
# These environment variables link the API to the database service defined below
- SPRING_DATASOURCE_URL=jdbc:postgresql://r710.dev.gofwd.group:5433/ss_builder
- SPRING_DATASOURCE_USERNAME=dba
- SPRING_DATASOURCE_PASSWORD=!@#Qwerty
networks:
- app-network
# --- 2. Next.js App Service (Frontend) ---
nextjs-app:
build:
context: ./frontend # Path to your Next.js project's root folder
dockerfile: Dockerfile # Assumes you have a Dockerfile in ./frontend
container_name: ss_builder-app
ports:
- "3000:3000" # Map host port 3000 to container port 3000
environment:
# This variable is crucial: Next.js needs the URL for the Spring API
# Use the Docker internal service name 'spring-api' and its port 8080
- NEXT_PUBLIC_API_URL=http://ss_builder-api:8080
# For local testing, you might need the host IP for Next.js to call back
# - NEXT_PUBLIC_API_URL_LOCAL=http://localhost:8080
depends_on:
- ss_builder-api
networks:
- app-network
# --- Docker Network for Inter-Container Communication ---
networks:
app-network:
driver: bridge

View File

@@ -0,0 +1,22 @@
# Stage 1: Build the static assets
FROM node:20-alpine as builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
# Run the Next.js build command
RUN npm run build
# Stage 2: Run the production application (Next.js server)
FROM node:20-alpine
WORKDIR /app
# Copy only the necessary files for running the app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/public ./public
# Set environment variables
ENV NODE_ENV production
EXPOSE 3000
# Run the Next.js production server
CMD ["npm", "start"]

213
importLogic.md Normal file
View File

@@ -0,0 +1,213 @@
# Ballistic Import Pipeline
A high-level overview of how merchant data flows through the Spring ETL system.
---
## Purpose
This document explains how the Ballistic backend:
1. Fetches merchant product feeds (CSV/TSV)
2. Normalizes raw data into structured entities
3. Updates products and offers in an idempotent way
4. Supports two sync modes:
- Full Import
- Offer-Only Sync
---
# 1. High-Level Flow
## ASCII Diagram
```
┌──────────────────────────┐
│ /admin/imports/{id} │
│ (Full Import Trigger) │
└─────────────┬────────────┘
┌──────────────────────────────┐
│ importMerchantFeed(merchantId)│
└─────────────┬────────────────┘
┌────────────────────────────────────────────────────────┐
│ readFeedRowsForMerchant() │
│ - auto-detect delimiter │
│ - parse CSV/TSV → MerchantFeedRow objects │
└─────────────────┬──────────────────────────────────────┘
│ List<MerchantFeedRow>
┌──────────────────────────────────────┐
│ For each MerchantFeedRow row: │
│ resolveBrand() │
│ upsertProduct() │
│ - find existing via brand+mpn/upc │
│ - update fields (mapped partRole) │
│ upsertOfferFromRow() │
└──────────────────────────────────────┘
```
---
# 2. Full Import Explained
Triggered by:
```
POST /admin/imports/{merchantId}
```
### Step 1 — Load merchant
Using `merchantRepository.findById()`.
### Step 2 — Parse feed rows
`readFeedRowsForMerchant()`:
- Auto-detects delimiter (`\t`, `,`, `;`)
- Validates required headers
- Parses each row into `MerchantFeedRow`
### Step 3 — Process each row
For each parsed row:
#### a. resolveBrand()
- Finds or creates brand
- Defaults to “Aero Precision” if missing
#### b. upsertProduct()
Dedupes by:
1. Brand + MPN
2. Brand + UPC (currently SKU placeholder)
If no match → create new product.
Then applies:
- Name + slug
- Descriptions
- Images
- MPN/identifiers
- Platform inference
- Category mapping
- Part role inference
#### c. upsertOfferFromRow()
Creates or updates a ProductOffer:
- Prices
- Stock
- Buy URL
- lastSeenAt
- firstSeenAt when newly created
Idempotent — does not duplicate offers.
---
# 3. Offer-Only Sync
Triggered by:
```
POST /admin/imports/{merchantId}/offers-only
```
Does NOT:
- Create products
- Update product fields
It only updates:
- price
- originalPrice
- inStock
- buyUrl
- lastSeenAt
If the offer does not exist, it is skipped.
---
# 4. Auto-Detecting CSV/TSV Parser
The parser:
- Attempts multiple delimiters
- Validates headers
- Handles malformed or short rows
- Never throws on missing columns
- Returns clean MerchantFeedRow objects
Designed for messy merchant feeds.
---
# 5. Entities Updated During Import
### Product
- name
- slug
- short/long description
- main image
- mpn
- upc (future)
- platform
- rawCategoryKey
- partRole
### ProductOffer
- merchant
- product
- avantlinkProductId (SKU placeholder)
- price
- originalPrice
- inStock
- buyUrl
- lastSeenAt
- firstSeenAt
### Merchant
- lastFullImportAt
- lastOfferSyncAt
---
# 6. Extension Points
You can extend the import pipeline in these areas:
- Add per-merchant column mapping
- Add true UPC parsing
- Support multi-platform parts
- Improve partRole inference
- Implement global deduplication across merchants
---
# 7. Quick Reference: Main Methods
| Method | Purpose |
|--------|---------|
| importMerchantFeed | Full product + offer import |
| readFeedRowsForMerchant | Detect delimiter + parse feed |
| resolveBrand | Normalize brand names |
| upsertProduct | Idempotent product write |
| updateProductFromRow | Apply product fields |
| upsertOfferFromRow | Idempotent offer write |
| syncOffersOnly | Offer-only sync |
| upsertOfferOnlyFromRow | Update existing offers |
| detectCsvFormat | Auto-detect delimiter |
| fetchFeedRows | Simpler parser for offers |
---
# 8. Summary
The Ballistic importer is:
- Robust against bad data
- Idempotent and safe
- Flexible for multiple merchants
- Extensible for long-term scaling
This pipeline powers the product catalog and offer data for the Ballistic ecosystem.

66
pom.xml
View File

@@ -1,22 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
<relativePath/>
</parent>
<groupId>group.goforward</groupId>
<artifactId>ballistic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ballistic</name>
<description>Ballistic Builder API</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer>
<name>Don Strawsburg</name>
@@ -29,27 +35,34 @@
<organization>Forward Group, LLC</organization>
</developer>
</developers>
<scm>
<connection/>
<developerConnection/>
<connection></connection>
<developerConnection>scm:git:https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git</developerConnection>
<tag/>
<url>ssh://git@gitea.gofwd.group:2225/Forward_Group/ballistic-builder-spring.git</url>
</scm>
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
@@ -61,33 +74,63 @@
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.5</version>
</dependency>
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>-->
<!-- Jakarta / validation API is pulled by starters; explicit jakarta persistence if needed -->
<!-- Jakarta persistence API (optional, JPA starter already brings it transitively) -->
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.11.0</version>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JSON Web Tokens (JJWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
@@ -101,6 +144,7 @@
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>

View File

@@ -0,0 +1,159 @@
-- Add timestamp columns (created_at, updated_at, deleted_at) to all tables
-- PostgreSQL script for ballistic-builder-spring project
-- accounts
ALTER TABLE accounts
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_accounts_updated_at BEFORE UPDATE ON accounts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- affiliate_category_map
ALTER TABLE affiliate_category_map
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_affiliate_category_map_updated_at BEFORE UPDATE ON affiliate_category_map FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- brands
ALTER TABLE brands
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_brands_updated_at BEFORE UPDATE ON brands FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- builds
ALTER TABLE build_items
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_builds_items_updated_at BEFORE UPDATE ON build_items FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- builds
ALTER TABLE builds
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_builds_updated_at BEFORE UPDATE ON builds FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- category_mappings
ALTER TABLE category_mappings
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_category_mappings_updated_at BEFORE UPDATE ON category_mappings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- feed_imports
ALTER TABLE feed_imports
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_feed_imports_updated_at BEFORE UPDATE ON feed_imports FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- merchants
ALTER TABLE merchants
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_merchants_updated_at BEFORE UPDATE ON merchants FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- merchant_category_mappings (truncated table name in search results)
ALTER TABLE merchant_category_mappings
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_category_mappings_updated_at BEFORE UPDATE ON category_mappings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- merchant_category_mappings (truncated table name in search results)
ALTER TABLE merchant_category_map
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_merchant_category_map_updated_at BEFORE UPDATE ON merchant_category_map FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- part_categories
ALTER TABLE part_categories
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_part_categories_updated_at BEFORE UPDATE ON part_categories FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- part_role_mappings
ALTER TABLE part_role_mappings
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_part_role_mappings_updated_at BEFORE UPDATE ON part_role_mappings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- part_role_mappings
ALTER TABLE platforms
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_platforms_updated_at BEFORE UPDATE ON platforms FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- price_history
ALTER TABLE price_history
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_price_history_updated_at BEFORE UPDATE ON price_history FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
--
-- product_offers
ALTER TABLE product_offers
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_product_offers_updated_at BEFORE UPDATE ON product_offers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- products
ALTER TABLE products
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_products_updated_at BEFORE UPDATE ON products FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- states
ALTER TABLE states
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_states_updated_at BEFORE UPDATE ON states FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- users
ALTER TABLE users
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE;
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Create function to automatically update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Create triggers for all tables to auto-update updated_at on row modification
CREATE TRIGGER update_accounts_updated_at BEFORE UPDATE ON accounts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_affiliate_category_map_updated_at BEFORE UPDATE ON affiliate_category_map FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_brands_updated_at BEFORE UPDATE ON brands FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_builds_updated_at BEFORE UPDATE ON builds FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_builds_items_updated_at BEFORE UPDATE ON build_items FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_merchants_updated_at BEFORE UPDATE ON merchants FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_merchant_category_mappings_updated_at BEFORE UPDATE ON merchant_category_mappings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_part_role_mappings_updated_at BEFORE UPDATE ON part_role_mappings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_price_history_updated_at BEFORE UPDATE ON price_history FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_product_offers_updated_at BEFORE UPDATE ON product_offers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_products_updated_at BEFORE UPDATE ON products FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

52265
sql/ddl.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,9 @@ package group.goforward.ballistic;
import java.time.LocalDateTime;
/**
* @param <T>
*/
public class ApiResponse<T> {
private static final String API_SUCCESS = "success";

View File

@@ -3,19 +3,16 @@ package group.goforward.ballistic;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration
@EntityScan("group.goforward.ballistic.model")
@ComponentScan("group.goforward.ballistic.configuration")
@ComponentScan("group.goforward.ballistic.controllers")
@ComponentScan("group.goforward.ballistic.service")
@SpringBootApplication
@EnableCaching
@EntityScan(basePackages = "group.goforward.ballistic.model")
@EnableJpaRepositories(basePackages = "group.goforward.ballistic.repos")
public class BallisticApplication {
public static void main(String[] args) {
SpringApplication.run(BallisticApplication.class, args);
}
}

View File

@@ -0,0 +1,16 @@
package group.goforward.ballistic.configuration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
// Simple in-memory cache for dev/local
return new ConcurrentMapCacheManager("gunbuilderProducts");
}
}

View File

@@ -26,14 +26,14 @@ public class CorsConfig {
"http://localhost:4201",
"http://localhost:8070",
"https://localhost:8070",
"http://192.168.11.210:8070",
"https://192.168.11.210:8070",
"http://localhost:4200",
"http://citysites.gofwd.group",
"https://citysites.gofwd.group",
"http://citysites.gofwd.group:8070",
"https://citysites.gofwd.group: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

View File

@@ -0,0 +1,16 @@
package group.goforward.ballistic.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
// @Configuration
// public class PasswordConfig {
// @Bean
// public PasswordEncoder passwordEncoder() {
// // BCrypt default password
// return new BCryptPasswordEncoder();
// }
// }

View File

@@ -0,0 +1,59 @@
package group.goforward.ballistic.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sm ->
sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
// Auth endpoints always open
.requestMatchers("/api/auth/**").permitAll()
// Swagger / docs
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
// Health
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
// Public product endpoints
.requestMatchers("/api/products/gunbuilder/**").permitAll()
// Everything else (for now) also open we can tighten later
.anyRequest().permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt is a solid default for user passwords
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration configuration
) throws Exception {
return configuration.getAuthenticationManager();
}
}

View File

@@ -1 +1,13 @@
/**
* Provides the classes necessary for the Spring Configurations for the ballistic -Builder application.
* This package includes Configurations for Spring-Boot application
*
*
* <p>The main entry point for managing the inventory is the
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
*
* @since 1.0
* @author Don Strawsburg
* @version 1.1
*/
package group.goforward.ballistic.configuration;

View File

@@ -0,0 +1,102 @@
package group.goforward.ballistic.controllers;
import group.goforward.ballistic.model.User;
import group.goforward.ballistic.repos.UserRepository;
import group.goforward.ballistic.security.JwtService;
import group.goforward.ballistic.web.dto.auth.AuthResponse;
import group.goforward.ballistic.web.dto.auth.LoginRequest;
import group.goforward.ballistic.web.dto.auth.RegisterRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.time.OffsetDateTime;
import java.util.UUID;
@RestController
@RequestMapping("/api/auth")
@CrossOrigin
public class AuthController {
private final UserRepository users;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
public AuthController(
UserRepository users,
PasswordEncoder passwordEncoder,
JwtService jwtService
) {
this.users = users;
this.passwordEncoder = passwordEncoder;
this.jwtService = jwtService;
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
String email = request.getEmail().trim().toLowerCase();
if (users.existsByEmailIgnoreCaseAndDeletedAtIsNull(email)) {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body("Email is already registered");
}
User user = new User();
// Let DB generate id
user.setUuid(UUID.randomUUID());
user.setEmail(email);
user.setPasswordHash(passwordEncoder.encode(request.getPassword()));
user.setDisplayName(request.getDisplayName());
user.setRole("USER");
user.setIsActive(true);
user.setCreatedAt(OffsetDateTime.now());
user.setUpdatedAt(OffsetDateTime.now());
users.save(user);
String token = jwtService.generateToken(user);
AuthResponse response = new AuthResponse(
token,
user.getEmail(),
user.getDisplayName(),
user.getRole()
);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
String email = request.getEmail().trim().toLowerCase();
User user = users.findByEmailIgnoreCaseAndDeletedAtIsNull(email)
.orElse(null);
if (user == null || !user.getIsActive()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
}
if (!passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
}
user.setLastLoginAt(OffsetDateTime.now());
user.incrementLoginCount();
user.setUpdatedAt(OffsetDateTime.now());
users.save(user);
String token = jwtService.generateToken(user);
AuthResponse response = new AuthResponse(
token,
user.getEmail(),
user.getDisplayName(),
user.getRole()
);
return ResponseEntity.ok(response);
}
}

View File

@@ -0,0 +1,51 @@
package group.goforward.ballistic.controllers;
import group.goforward.ballistic.model.Brand;
import group.goforward.ballistic.model.State;
import group.goforward.ballistic.repos.BrandRepository;
import group.goforward.ballistic.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/brands")
public class BrandController {
@Autowired
private BrandRepository repo;
@Autowired
private BrandService brandService;
//@Cacheable(value="getAllStates")
@GetMapping("/all")
public ResponseEntity<List<Brand>> getAllBrands() {
List<Brand> brand = repo.findAll();
return ResponseEntity.ok(brand);
}
@GetMapping("/{id}")
public ResponseEntity<Brand> getAllBrandsById(@PathVariable Integer id) {
return repo.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping("/add")
public ResponseEntity<Brand> createbrand(@RequestBody Brand item) {
Brand created = brandService.save(item);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@DeleteMapping("/delete/{id}")
public ResponseEntity<Void> deleteItem(@PathVariable Integer id) {
return brandService.findById(id)
.map(item -> {
brandService.deleteById(id);
return ResponseEntity.noContent().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
}
}

View File

@@ -0,0 +1,34 @@
package group.goforward.ballistic.controllers;
import group.goforward.ballistic.repos.PartCategoryRepository;
import group.goforward.ballistic.web.dto.admin.PartCategoryDto;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/categories")
@CrossOrigin // you can tighten origins later
public class CategoryController {
private final PartCategoryRepository partCategories;
public CategoryController(PartCategoryRepository partCategories) {
this.partCategories = partCategories;
}
@GetMapping
public List<PartCategoryDto> list() {
return partCategories.findAllByOrderByGroupNameAscSortOrderAscNameAsc()
.stream()
.map(pc -> new PartCategoryDto(
pc.getId(),
pc.getSlug(),
pc.getName(),
pc.getDescription(),
pc.getGroupName(),
pc.getSortOrder()
))
.toList();
}
}

View File

@@ -0,0 +1,39 @@
package group.goforward.ballistic.controllers;
import group.goforward.ballistic.services.MerchantFeedImportService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/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<Void> 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<Void> syncOffersOnly(@PathVariable Integer merchantId) {
merchantFeedImportService.syncOffersOnly(merchantId);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,63 @@
// MerchantAdminController.java
package group.goforward.ballistic.controllers;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import group.goforward.ballistic.model.Merchant;
import group.goforward.ballistic.repos.MerchantRepository;
import group.goforward.ballistic.web.dto.MerchantAdminDto;
import org.springframework.web.bind.annotation.*;
import java.time.OffsetDateTime;
import java.util.List;
@RestController
@RequestMapping("/admin/merchants")
@CrossOrigin(origins = "http://localhost:3000") // TEMP for Cross-Origin Bug
public class MerchantAdminController {
private final MerchantRepository merchantRepository;
public MerchantAdminController(MerchantRepository merchantRepository) {
this.merchantRepository = merchantRepository;
}
@GetMapping
public List<MerchantAdminDto> listMerchants() {
return merchantRepository.findAll().stream().map(this::toDto).toList();
}
@PutMapping("/{id}")
public MerchantAdminDto updateMerchant(
@PathVariable Integer id,
@RequestBody MerchantAdminDto payload
) {
Merchant merchant = merchantRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Merchant not found"));
merchant.setFeedUrl(payload.getFeedUrl());
merchant.setOfferFeedUrl(payload.getOfferFeedUrl());
merchant.setIsActive(payload.getIsActive() != null ? payload.getIsActive() : true);
// dont touch last* here; those are set by import jobs
merchant = merchantRepository.save(merchant);
return toDto(merchant);
}
private MerchantAdminDto toDto(Merchant m) {
MerchantAdminDto dto = new MerchantAdminDto();
dto.setId(m.getId());
dto.setName(m.getName());
dto.setFeedUrl(m.getFeedUrl());
dto.setOfferFeedUrl(m.getOfferFeedUrl());
dto.setIsActive(m.getIsActive());
dto.setLastFullImportAt(m.getLastFullImportAt());
dto.setLastOfferSyncAt(m.getLastOfferSyncAt());
return dto;
}
}

View File

@@ -0,0 +1,65 @@
package group.goforward.ballistic.controllers;
import group.goforward.ballistic.model.Merchant;
import group.goforward.ballistic.model.MerchantCategoryMapping;
import group.goforward.ballistic.repos.MerchantRepository;
import group.goforward.ballistic.services.MerchantCategoryMappingService;
import group.goforward.ballistic.web.dto.MerchantCategoryMappingDto;
import group.goforward.ballistic.web.dto.UpsertMerchantCategoryMappingRequest;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/admin/merchant-category-mappings")
@CrossOrigin
public class MerchantCategoryMappingController {
private final MerchantCategoryMappingService mappingService;
private final MerchantRepository merchantRepository;
public MerchantCategoryMappingController(
MerchantCategoryMappingService mappingService,
MerchantRepository merchantRepository
) {
this.mappingService = mappingService;
this.merchantRepository = merchantRepository;
}
@GetMapping
public List<MerchantCategoryMappingDto> listMappings(
@RequestParam("merchantId") Integer merchantId
) {
List<MerchantCategoryMapping> mappings = mappingService.findByMerchant(merchantId);
return mappings.stream()
.map(this::toDto)
.collect(Collectors.toList());
}
@PostMapping
public MerchantCategoryMappingDto upsertMapping(
@RequestBody UpsertMerchantCategoryMappingRequest request
) {
Merchant merchant = merchantRepository
.findById(request.getMerchantId())
.orElseThrow(() -> new IllegalArgumentException("Merchant not found: " + request.getMerchantId()));
MerchantCategoryMapping mapping = mappingService.upsertMapping(
merchant,
request.getRawCategory(),
request.getMappedPartRole()
);
return toDto(mapping);
}
private MerchantCategoryMappingDto toDto(MerchantCategoryMapping mapping) {
MerchantCategoryMappingDto dto = new MerchantCategoryMappingDto();
dto.setId(mapping.getId());
dto.setMerchantId(mapping.getMerchant().getId());
dto.setMerchantName(mapping.getMerchant().getName());
dto.setRawCategory(mapping.getRawCategory());
dto.setMappedPartRole(mapping.getMappedPartRole());
return dto;
}
}

View File

@@ -0,0 +1,23 @@
package group.goforward.ballistic.controllers;
import group.goforward.ballistic.model.Merchant;
import group.goforward.ballistic.repos.MerchantRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class MerchantDebugController {
private final MerchantRepository merchantRepository;
public MerchantDebugController(MerchantRepository merchantRepository) {
this.merchantRepository = merchantRepository;
}
@GetMapping("/admin/debug/merchants")
public List<Merchant> listMerchants() {
return merchantRepository.findAll();
}
}

View File

@@ -0,0 +1,13 @@
package group.goforward.ballistic.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PingController {
@GetMapping("/ping")
public String ping() {
return "pong";
}
}

View File

@@ -0,0 +1,137 @@
package group.goforward.ballistic.controllers;
import group.goforward.ballistic.model.Product;
import group.goforward.ballistic.model.ProductOffer;
import group.goforward.ballistic.repos.ProductOfferRepository;
import group.goforward.ballistic.web.dto.ProductOfferDto;
import group.goforward.ballistic.repos.ProductRepository;
import group.goforward.ballistic.web.dto.ProductSummaryDto;
import group.goforward.ballistic.web.mapper.ProductMapper;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/products")
@CrossOrigin
public class ProductController {
private final ProductRepository productRepository;
private final ProductOfferRepository productOfferRepository;
public ProductController(
ProductRepository productRepository,
ProductOfferRepository productOfferRepository
) {
this.productRepository = productRepository;
this.productOfferRepository = productOfferRepository;
}
@GetMapping("/gunbuilder")
@Cacheable(
value = "gunbuilderProducts",
key = "#platform + '::' + (#partRoles == null ? 'ALL' : #partRoles.toString())"
)
public List<ProductSummaryDto> getGunbuilderProducts(
@RequestParam(defaultValue = "AR-15") String platform,
@RequestParam(required = false, name = "partRoles") List<String> partRoles
) {
long started = System.currentTimeMillis();
System.out.println("getGunbuilderProducts: start, platform=" + platform +
", partRoles=" + (partRoles == null ? "null" : partRoles));
// 1) Load products (with brand pre-fetched)
long tProductsStart = System.currentTimeMillis();
List<Product> products;
if (partRoles == null || partRoles.isEmpty()) {
products = productRepository.findByPlatformWithBrand(platform);
} else {
products = productRepository.findByPlatformAndPartRoleInWithBrand(platform, partRoles);
}
long tProductsEnd = System.currentTimeMillis();
System.out.println("getGunbuilderProducts: loaded products: " +
products.size() + " in " + (tProductsEnd - tProductsStart) + " ms");
if (products.isEmpty()) {
long took = System.currentTimeMillis() - started;
System.out.println("getGunbuilderProducts: 0 products in " + took + " ms");
return List.of();
}
// 2) Load offers for these product IDs
long tOffersStart = System.currentTimeMillis();
List<Integer> productIds = products.stream()
.map(Product::getId)
.toList();
List<ProductOffer> allOffers =
productOfferRepository.findByProductIdIn(productIds);
long tOffersEnd = System.currentTimeMillis();
System.out.println("getGunbuilderProducts: loaded offers: " +
allOffers.size() + " in " + (tOffersEnd - tOffersStart) + " ms");
Map<Integer, List<ProductOffer>> offersByProductId = allOffers.stream()
.collect(Collectors.groupingBy(o -> o.getProduct().getId()));
// 3) Map to DTOs with price and buyUrl
long tMapStart = System.currentTimeMillis();
List<ProductSummaryDto> result = products.stream()
.map(p -> {
List<ProductOffer> offersForProduct =
offersByProductId.getOrDefault(p.getId(), Collections.emptyList());
ProductOffer bestOffer = pickBestOffer(offersForProduct);
BigDecimal price = bestOffer != null ? bestOffer.getEffectivePrice() : null;
String buyUrl = bestOffer != null ? bestOffer.getBuyUrl() : null;
return ProductMapper.toSummary(p, price, buyUrl);
})
.toList();
long tMapEnd = System.currentTimeMillis();
long took = System.currentTimeMillis() - started;
System.out.println("getGunbuilderProducts: mapping to DTOs took " +
(tMapEnd - tMapStart) + " ms");
System.out.println("getGunbuilderProducts: TOTAL " + took + " ms (" +
"products=" + (tProductsEnd - tProductsStart) + " ms, " +
"offers=" + (tOffersEnd - tOffersStart) + " ms, " +
"map=" + (tMapEnd - tMapStart) + " ms)");
return result;
}
@GetMapping("/{id}/offers")
public List<ProductOfferDto> getOffersForProduct(@PathVariable("id") Integer productId) {
List<ProductOffer> offers = productOfferRepository.findByProductId(productId);
return offers.stream()
.map(offer -> {
ProductOfferDto dto = new ProductOfferDto();
dto.setId(offer.getId().toString());
dto.setMerchantName(offer.getMerchant().getName());
dto.setPrice(offer.getEffectivePrice());
dto.setOriginalPrice(offer.getOriginalPrice());
dto.setInStock(Boolean.TRUE.equals(offer.getInStock()));
dto.setBuyUrl(offer.getBuyUrl());
dto.setLastUpdated(offer.getLastSeenAt());
return dto;
})
.toList();
}
private ProductOffer pickBestOffer(List<ProductOffer> offers) {
if (offers == null || offers.isEmpty()) {
return null;
}
// Right now: lowest price wins, regardless of stock (we set inStock=true on import anyway)
return offers.stream()
.filter(o -> o.getEffectivePrice() != null)
.min(Comparator.comparing(ProductOffer::getEffectivePrice))
.orElse(null);
}
}

View File

@@ -1,57 +0,0 @@
package group.goforward.ballistic.controllers;
import group.goforward.ballistic.model.Psa;
import group.goforward.ballistic.service.PsaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@RestController
@RequestMapping("/api/psa")
public class PsaController {
private final PsaService psaService;
@Autowired
public PsaController(PsaService psaService) {
this.psaService = psaService;
}
@GetMapping("/api/getAllPsa")
public List<Psa> getAllPsa() {
return psaService.findAll();
}
@GetMapping("/api/getPSAById/{id}")
public ResponseEntity<Psa> getPsaById(@PathVariable UUID id) {
Optional<Psa> psa = psaService.findById(id);
return psa.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
}
@PostMapping
public Psa createPsa(@RequestBody Psa psa) {
return psaService.save(psa);
}
@PutMapping("/api/updatePsa/{id}")
public ResponseEntity<Psa> updatePsa(@PathVariable UUID id, @RequestBody Psa psaDetails) {
Optional<Psa> psa = psaService.findById(id);
if (psa.isPresent()) {
Psa updatedPsa = psa.get();
// Update fields of the Psa entity as needed
return ResponseEntity.ok(psaService.save(updatedPsa));
} else {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/api/deletePsa/{id}")
public ResponseEntity<Void> deletePsa(@PathVariable UUID id) {
psaService.deleteById(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -3,8 +3,9 @@ package group.goforward.ballistic.controllers;
import group.goforward.ballistic.ApiResponse;
import group.goforward.ballistic.model.State;
import group.goforward.ballistic.repos.StateRepository;
import group.goforward.ballistic.service.StatesService;
import group.goforward.ballistic.services.StatesService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -13,44 +14,38 @@ import java.util.List;
@RestController
@RequestMapping()
@RequestMapping("/api/states")
public class StateController {
@Autowired
private StateRepository repo;
@Autowired
private StatesService statesService;
@GetMapping("/api/getAllStates")
//@Cacheable(value="getAllStates")
@GetMapping("/all")
public ResponseEntity<List<State>> getAllStates() {
List<State> state = repo.findAll();
return ResponseEntity.ok(state);
}
@GetMapping("/api/getAllStatesTest")
public ApiResponse<List<State>> getAllStatesTest() {
List<State> state = repo.findAll();
return ApiResponse.success(state);
}
@GetMapping("/api/getAllStatesById/{id}")
@GetMapping("/{id}")
public ResponseEntity<State> getAllStatesById(@PathVariable Integer id) {
return repo.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/api/getAllStatesByAbbreviation/{abbreviation}")
@GetMapping("/byAbbrev/{abbreviation}")
public ResponseEntity<State> getAllStatesByAbbreviation(@PathVariable String abbreviation) {
return repo.findByAbbreviation(abbreviation)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping("/api/addState")
@PostMapping("/addState")
public ResponseEntity<State> createState(@RequestBody State item) {
State created = statesService.save(item);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@DeleteMapping("/api/deleteState/{id}")
@DeleteMapping("/deleteState/{id}")
public ResponseEntity<Void> deleteItem(@PathVariable Integer id) {
return statesService.findById(id)
.map(item -> {

View File

@@ -0,0 +1,53 @@
package group.goforward.ballistic.controllers;
import group.goforward.ballistic.model.User;
import group.goforward.ballistic.repos.UserRepository;
import group.goforward.ballistic.services.UsersService;
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/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<List<User>> getAllUsers() {
List<User> data = repo.findAll();
return ResponseEntity.ok(data);
}
@GetMapping("/byId/{id}")
public ResponseEntity<User> getAllStatesById(@PathVariable Integer id) {
return repo.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping("/addUser")
public ResponseEntity<User> createUser(@RequestBody User item) {
User created = usersService.save(item);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@DeleteMapping("/deleteUser/{id}")
public ResponseEntity<Void> deleteItem(@PathVariable Integer id) {
return usersService.findById(id)
.map(item -> {
usersService.deleteById(id);
return ResponseEntity.noContent().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
}
}

View File

@@ -0,0 +1,40 @@
package group.goforward.ballistic.controllers.admin;
import group.goforward.ballistic.model.PartCategory;
import group.goforward.ballistic.repos.PartCategoryRepository;
import group.goforward.ballistic.web.dto.admin.PartCategoryDto;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/admin/categories")
@CrossOrigin
public class AdminCategoryController {
private final PartCategoryRepository partCategories;
public AdminCategoryController(PartCategoryRepository partCategories) {
this.partCategories = partCategories;
}
@GetMapping
public List<PartCategoryDto> listCategories() {
return partCategories
.findAllByOrderByGroupNameAscSortOrderAscNameAsc()
.stream()
.map(this::toDto)
.toList();
}
private PartCategoryDto toDto(PartCategory entity) {
return new PartCategoryDto(
entity.getId(),
entity.getSlug(),
entity.getName(),
entity.getDescription(),
entity.getGroupName(),
entity.getSortOrder()
);
}
}

View File

@@ -0,0 +1,117 @@
package group.goforward.ballistic.controllers.admin;
import group.goforward.ballistic.model.CategoryMapping;
import group.goforward.ballistic.model.Merchant;
import group.goforward.ballistic.model.PartCategory;
import group.goforward.ballistic.repos.CategoryMappingRepository;
import group.goforward.ballistic.repos.MerchantRepository;
import group.goforward.ballistic.repos.PartCategoryRepository;
import group.goforward.ballistic.web.dto.admin.MerchantCategoryMappingDto;
import group.goforward.ballistic.web.dto.admin.SimpleMerchantDto;
import group.goforward.ballistic.web.dto.admin.UpdateMerchantCategoryMappingRequest;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@RestController
@RequestMapping("/api/admin/category-mappings")
@CrossOrigin // you can tighten origins later
public class AdminCategoryMappingController {
private final CategoryMappingRepository categoryMappingRepository;
private final MerchantRepository merchantRepository;
private final PartCategoryRepository partCategoryRepository;
public AdminCategoryMappingController(
CategoryMappingRepository categoryMappingRepository,
MerchantRepository merchantRepository,
PartCategoryRepository partCategoryRepository
) {
this.categoryMappingRepository = categoryMappingRepository;
this.merchantRepository = merchantRepository;
this.partCategoryRepository = partCategoryRepository;
}
/**
* Merchants that have at least one category_mappings row.
* Used for the "All Merchants" dropdown in the UI.
*/
@GetMapping("/merchants")
public List<SimpleMerchantDto> listMerchantsWithMappings() {
List<Merchant> merchants = categoryMappingRepository.findDistinctMerchantsWithMappings();
return merchants.stream()
.map(m -> new SimpleMerchantDto(m.getId(), m.getName()))
.toList();
}
/**
* List mappings for a specific merchant, or all mappings if no merchantId is provided.
* GET /api/admin/category-mappings?merchantId=1
*/
@GetMapping
public List<MerchantCategoryMappingDto> listByMerchant(
@RequestParam(name = "merchantId", required = false) Integer merchantId
) {
List<CategoryMapping> mappings;
if (merchantId != null) {
mappings = categoryMappingRepository.findByMerchantIdOrderByRawCategoryPathAsc(merchantId);
} else {
// fall back to all mappings; you can add a more specific repository method later if desired
mappings = categoryMappingRepository.findAll();
}
return mappings.stream()
.map(cm -> new MerchantCategoryMappingDto(
cm.getId(),
cm.getMerchant().getId(),
cm.getMerchant().getName(),
cm.getRawCategoryPath(),
cm.getPartCategory() != null ? cm.getPartCategory().getId() : null,
cm.getPartCategory() != null ? cm.getPartCategory().getName() : null
))
.toList();
}
/**
* Update a single mapping's part_category.
* POST /api/admin/category-mappings/{id}
* Body: { "partCategoryId": 24 }
*/
@PostMapping("/{id}")
public MerchantCategoryMappingDto updateMapping(
@PathVariable Integer id,
@RequestBody UpdateMerchantCategoryMappingRequest request
) {
CategoryMapping mapping = categoryMappingRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Mapping not found"));
PartCategory partCategory = null;
if (request.partCategoryId() != null) {
partCategory = partCategoryRepository.findById(request.partCategoryId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Part category not found"));
}
mapping.setPartCategory(partCategory);
mapping = categoryMappingRepository.save(mapping);
return new MerchantCategoryMappingDto(
mapping.getId(),
mapping.getMerchant().getId(),
mapping.getMerchant().getName(),
mapping.getRawCategoryPath(),
mapping.getPartCategory() != null ? mapping.getPartCategory().getId() : null,
mapping.getPartCategory() != null ? mapping.getPartCategory().getName() : null
);
}
@PutMapping("/{id}")
public MerchantCategoryMappingDto updateMappingPut(
@PathVariable Integer id,
@RequestBody UpdateMerchantCategoryMappingRequest request
) {
// just delegate so POST & PUT behave the same
return updateMapping(id, request);
}
}

View File

@@ -0,0 +1,123 @@
package group.goforward.ballistic.controllers.admin;
import group.goforward.ballistic.model.PartCategory;
import group.goforward.ballistic.model.PartRoleMapping;
import group.goforward.ballistic.repos.PartCategoryRepository;
import group.goforward.ballistic.repos.PartRoleMappingRepository;
import group.goforward.ballistic.web.dto.admin.AdminPartRoleMappingDto;
import group.goforward.ballistic.web.dto.admin.CreatePartRoleMappingRequest;
import group.goforward.ballistic.web.dto.admin.UpdatePartRoleMappingRequest;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@RestController
@RequestMapping("/api/admin/part-role-mappings")
@CrossOrigin
public class AdminPartRoleMappingController {
private final PartRoleMappingRepository partRoleMappingRepository;
private final PartCategoryRepository partCategoryRepository;
public AdminPartRoleMappingController(
PartRoleMappingRepository partRoleMappingRepository,
PartCategoryRepository partCategoryRepository
) {
this.partRoleMappingRepository = partRoleMappingRepository;
this.partCategoryRepository = partCategoryRepository;
}
// GET /api/admin/part-role-mappings?platform=AR-15
@GetMapping
public List<AdminPartRoleMappingDto> list(
@RequestParam(name = "platform", required = false) String platform
) {
List<PartRoleMapping> mappings;
if (platform != null && !platform.isBlank()) {
mappings = partRoleMappingRepository.findByPlatformOrderByPartRoleAsc(platform);
} else {
mappings = partRoleMappingRepository.findAll();
}
return mappings.stream()
.map(this::toDto)
.toList();
}
// POST /api/admin/part-role-mappings
@PostMapping
public AdminPartRoleMappingDto create(
@RequestBody CreatePartRoleMappingRequest request
) {
PartCategory category = partCategoryRepository.findBySlug(request.categorySlug())
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"PartCategory not found for slug: " + request.categorySlug()
));
PartRoleMapping mapping = new PartRoleMapping();
mapping.setPlatform(request.platform());
mapping.setPartRole(request.partRole());
mapping.setPartCategory(category);
mapping.setNotes(request.notes());
mapping = partRoleMappingRepository.save(mapping);
return toDto(mapping);
}
// PUT /api/admin/part-role-mappings/{id}
@PutMapping("/{id}")
public AdminPartRoleMappingDto update(
@PathVariable Integer id,
@RequestBody UpdatePartRoleMappingRequest request
) {
PartRoleMapping mapping = partRoleMappingRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Mapping not found"));
if (request.platform() != null) {
mapping.setPlatform(request.platform());
}
if (request.partRole() != null) {
mapping.setPartRole(request.partRole());
}
if (request.categorySlug() != null) {
PartCategory category = partCategoryRepository.findBySlug(request.categorySlug())
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"PartCategory not found for slug: " + request.categorySlug()
));
mapping.setPartCategory(category);
}
if (request.notes() != null) {
mapping.setNotes(request.notes());
}
mapping = partRoleMappingRepository.save(mapping);
return toDto(mapping);
}
// DELETE /api/admin/part-role-mappings/{id}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Integer id) {
if (!partRoleMappingRepository.existsById(id)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Mapping not found");
}
partRoleMappingRepository.deleteById(id);
}
private AdminPartRoleMappingDto toDto(PartRoleMapping mapping) {
PartCategory cat = mapping.getPartCategory();
return new AdminPartRoleMappingDto(
mapping.getId(),
mapping.getPlatform(),
mapping.getPartRole(),
cat != null ? cat.getSlug() : null,
cat != null ? cat.getGroupName() : null,
mapping.getNotes()
);
}
}

View File

@@ -0,0 +1,35 @@
package group.goforward.ballistic.controllers.admin;
import group.goforward.ballistic.model.PartCategory;
import group.goforward.ballistic.repos.PartCategoryRepository;
import group.goforward.ballistic.web.dto.admin.PartCategoryDto;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/admin/part-categories")
@CrossOrigin // keep it loose for now, you can tighten origins later
public class PartCategoryAdminController {
private final PartCategoryRepository partCategories;
public PartCategoryAdminController(PartCategoryRepository partCategories) {
this.partCategories = partCategories;
}
@GetMapping
public List<PartCategoryDto> list() {
return partCategories.findAllByOrderByGroupNameAscSortOrderAscNameAsc()
.stream()
.map(pc -> new PartCategoryDto(
pc.getId(),
pc.getSlug(),
pc.getName(),
pc.getDescription(),
pc.getGroupName(),
pc.getSortOrder()
))
.toList();
}
}

View File

@@ -0,0 +1,13 @@
/**
* Provides the classes necessary for the Spring Controllers for the ballistic -Builder application.
* This package includes Controllers for Spring-Boot application
*
*
* <p>The main entry point for managing the inventory is the
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
*
* @since 1.0
* @author Don Strawsburg
* @version 1.1
*/
package group.goforward.ballistic.controllers;

View File

@@ -0,0 +1,30 @@
package group.goforward.ballistic.imports;
import java.math.BigDecimal;
public record MerchantFeedRow(
String sku,
String manufacturerId,
String brandName,
String productName,
String longDescription,
String shortDescription,
String department,
String category,
String subCategory,
String thumbUrl,
String imageUrl,
String buyLink,
String keywords,
String reviews,
BigDecimal retailPrice,
BigDecimal salePrice,
String brandPageLink,
String brandLogoImage,
String productPageViewTracking,
String variantsXml,
String mediumImageUrl,
String productContentWidget,
String googleCategorization,
String itemBasedCommission
) {}

View File

@@ -0,0 +1,17 @@
package group.goforward.ballistic.imports.dto;
import java.math.BigDecimal;
public record MerchantFeedRow(
String brandName,
String productName,
String mpn,
String upc,
String avantlinkProductId,
String sku,
String categoryPath,
String buyUrl,
BigDecimal price,
BigDecimal originalPrice,
boolean inStock
) {}

View File

@@ -0,0 +1,13 @@
/**
* Provides the classes necessary for the Spring Data Transfer Objects for the ballistic -Builder application.
* This package includes DTO for Spring-Boot application
*
*
* <p>The main entry point for managing the inventory is the
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
*
* @since 1.0
* @author Sean Strawsburg
* @version 1.1
*/
package group.goforward.ballistic.imports.dto;

View File

@@ -6,6 +6,7 @@ import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@@ -53,6 +54,15 @@ public class Account {
@Column(name = "scope", length = Integer.MAX_VALUE)
private String scope;
@Column(name = "created_at")
private OffsetDateTime createdAt;
@Column(name = "updated_at")
private OffsetDateTime updatedAt;
@Column(name = "deleted_at")
private OffsetDateTime deletedAt;
public UUID getId() {
return id;
}
@@ -157,4 +167,28 @@ public class Account {
this.scope = scope;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public OffsetDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(OffsetDateTime deletedAt) {
this.deletedAt = deletedAt;
}
}

View File

@@ -1,293 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import java.math.BigDecimal;
import java.util.UUID;
@Entity
@Table(name = "aero_precision")
public class
AeroPrecision {
@Id
@Column(name = "sku", nullable = false, length = Integer.MAX_VALUE)
private String sku;
@Column(name = "manufacturer_id", length = Integer.MAX_VALUE)
private String manufacturerId;
@Column(name = "brand_name", length = Integer.MAX_VALUE)
private String brandName;
@Column(name = "product_name", length = Integer.MAX_VALUE)
private String productName;
@Column(name = "long_description", length = Integer.MAX_VALUE)
private String longDescription;
@Column(name = "short_description", length = Integer.MAX_VALUE)
private String shortDescription;
@Column(name = "department", length = Integer.MAX_VALUE)
private String department;
@Column(name = "category", length = Integer.MAX_VALUE)
private String category;
@Column(name = "subcategory", length = Integer.MAX_VALUE)
private String subcategory;
@Column(name = "thumb_url", length = Integer.MAX_VALUE)
private String thumbUrl;
@Column(name = "image_url", length = Integer.MAX_VALUE)
private String imageUrl;
@Column(name = "buy_link", length = Integer.MAX_VALUE)
private String buyLink;
@Column(name = "keywords", length = Integer.MAX_VALUE)
private String keywords;
@Column(name = "reviews", length = Integer.MAX_VALUE)
private String reviews;
@Column(name = "retail_price")
private BigDecimal retailPrice;
@Column(name = "sale_price")
private BigDecimal salePrice;
@Column(name = "brand_page_link", length = Integer.MAX_VALUE)
private String brandPageLink;
@Column(name = "brand_logo_image", length = Integer.MAX_VALUE)
private String brandLogoImage;
@Column(name = "product_page_view_tracking", length = Integer.MAX_VALUE)
private String productPageViewTracking;
@Column(name = "variants_xml", length = Integer.MAX_VALUE)
private String variantsXml;
@Column(name = "medium_image_url", length = Integer.MAX_VALUE)
private String mediumImageUrl;
@Column(name = "product_content_widget", length = Integer.MAX_VALUE)
private String productContentWidget;
@Column(name = "google_categorization", length = Integer.MAX_VALUE)
private String googleCategorization;
@Column(name = "item_based_commission", length = Integer.MAX_VALUE)
private String itemBasedCommission;
@ColumnDefault("gen_random_uuid()")
@Column(name = "uuid")
private UUID uuid;
public String getSku() {
return sku;
}
public void setSku(String sku) {
this.sku = sku;
}
public String getManufacturerId() {
return manufacturerId;
}
public void setManufacturerId(String manufacturerId) {
this.manufacturerId = manufacturerId;
}
public String getBrandName() {
return brandName;
}
public void setBrandName(String brandName) {
this.brandName = brandName;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getLongDescription() {
return longDescription;
}
public void setLongDescription(String longDescription) {
this.longDescription = longDescription;
}
public String getShortDescription() {
return shortDescription;
}
public void setShortDescription(String shortDescription) {
this.shortDescription = shortDescription;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getSubcategory() {
return subcategory;
}
public void setSubcategory(String subcategory) {
this.subcategory = subcategory;
}
public String getThumbUrl() {
return thumbUrl;
}
public void setThumbUrl(String thumbUrl) {
this.thumbUrl = thumbUrl;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getBuyLink() {
return buyLink;
}
public void setBuyLink(String buyLink) {
this.buyLink = buyLink;
}
public String getKeywords() {
return keywords;
}
public void setKeywords(String keywords) {
this.keywords = keywords;
}
public String getReviews() {
return reviews;
}
public void setReviews(String reviews) {
this.reviews = reviews;
}
public BigDecimal getRetailPrice() {
return retailPrice;
}
public void setRetailPrice(BigDecimal retailPrice) {
this.retailPrice = retailPrice;
}
public BigDecimal getSalePrice() {
return salePrice;
}
public void setSalePrice(BigDecimal salePrice) {
this.salePrice = salePrice;
}
public String getBrandPageLink() {
return brandPageLink;
}
public void setBrandPageLink(String brandPageLink) {
this.brandPageLink = brandPageLink;
}
public String getBrandLogoImage() {
return brandLogoImage;
}
public void setBrandLogoImage(String brandLogoImage) {
this.brandLogoImage = brandLogoImage;
}
public String getProductPageViewTracking() {
return productPageViewTracking;
}
public void setProductPageViewTracking(String productPageViewTracking) {
this.productPageViewTracking = productPageViewTracking;
}
public String getVariantsXml() {
return variantsXml;
}
public void setVariantsXml(String variantsXml) {
this.variantsXml = variantsXml;
}
public String getMediumImageUrl() {
return mediumImageUrl;
}
public void setMediumImageUrl(String mediumImageUrl) {
this.mediumImageUrl = mediumImageUrl;
}
public String getProductContentWidget() {
return productContentWidget;
}
public void setProductContentWidget(String productContentWidget) {
this.productContentWidget = productContentWidget;
}
public String getGoogleCategorization() {
return googleCategorization;
}
public void setGoogleCategorization(String googleCategorization) {
this.googleCategorization = googleCategorization;
}
public String getItemBasedCommission() {
return itemBasedCommission;
}
public void setItemBasedCommission(String itemBasedCommission) {
this.itemBasedCommission = itemBasedCommission;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
}

View File

@@ -2,26 +2,46 @@ package group.goforward.ballistic.model;
import jakarta.persistence.*;
import java.time.OffsetDateTime;
@Entity
@Table(name = "affiliate_category_map")
public class AffiliateCategoryMap {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "feedname", nullable = false, length = 100)
private String feedname;
// e.g. "PART_ROLE", "RAW_CATEGORY", etc.
@Column(name = "source_type", nullable = false)
private String sourceType;
@Column(name = "affiliatecategory", nullable = false)
private String affiliatecategory;
// the value were mapping from (e.g. "suppressor", "TRIGGER")
@Column(name = "source_value", nullable = false)
private String sourceValue;
@Column(name = "buildercategoryid", nullable = false)
private Integer buildercategoryid;
// optional platform ("AR-15", "PRECISION", etc.)
@Column(name = "platform")
private String platform;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "part_category_id", nullable = false)
private PartCategory partCategory;
@Column(name = "notes")
private String notes;
@Column(name = "created_at")
private OffsetDateTime createdAt;
@Column(name = "updated_at")
private OffsetDateTime updatedAt;
@Column(name = "deleted_at")
private OffsetDateTime deletedAt;
// --- getters / setters ---
public Integer getId() {
return id;
}
@@ -30,28 +50,36 @@ public class AffiliateCategoryMap {
this.id = id;
}
public String getFeedname() {
return feedname;
public String getSourceType() {
return sourceType;
}
public void setFeedname(String feedname) {
this.feedname = feedname;
public void setSourceType(String sourceType) {
this.sourceType = sourceType;
}
public String getAffiliatecategory() {
return affiliatecategory;
public String getSourceValue() {
return sourceValue;
}
public void setAffiliatecategory(String affiliatecategory) {
this.affiliatecategory = affiliatecategory;
public void setSourceValue(String sourceValue) {
this.sourceValue = sourceValue;
}
public Integer getBuildercategoryid() {
return buildercategoryid;
public String getPlatform() {
return platform;
}
public void setBuildercategoryid(Integer buildercategoryid) {
this.buildercategoryid = buildercategoryid;
public void setPlatform(String platform) {
this.platform = platform;
}
public PartCategory getPartCategory() {
return partCategory;
}
public void setPartCategory(PartCategory partCategory) {
this.partCategory = partCategory;
}
public String getNotes() {
@@ -62,4 +90,27 @@ public class AffiliateCategoryMap {
this.notes = notes;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public OffsetDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(OffsetDateTime deletedAt) {
this.deletedAt = deletedAt;
}
}

View File

@@ -1,88 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(name = "authenticator")
public class Authenticator {
@EmbeddedId
private AuthenticatorId id;
@Column(name = "\"providerAccountId\"", nullable = false, length = Integer.MAX_VALUE)
private String providerAccountId;
@Column(name = "\"credentialPublicKey\"", nullable = false, length = Integer.MAX_VALUE)
private String credentialPublicKey;
@Column(name = "counter", nullable = false)
private Integer counter;
@Column(name = "\"credentialDeviceType\"", nullable = false, length = Integer.MAX_VALUE)
private String credentialDeviceType;
@Column(name = "\"credentialBackedUp\"", nullable = false)
private Boolean credentialBackedUp = false;
@Column(name = "transports", length = Integer.MAX_VALUE)
private String transports;
public AuthenticatorId getId() {
return id;
}
public void setId(AuthenticatorId id) {
this.id = id;
}
public String getProviderAccountId() {
return providerAccountId;
}
public void setProviderAccountId(String providerAccountId) {
this.providerAccountId = providerAccountId;
}
public String getCredentialPublicKey() {
return credentialPublicKey;
}
public void setCredentialPublicKey(String credentialPublicKey) {
this.credentialPublicKey = credentialPublicKey;
}
public Integer getCounter() {
return counter;
}
public void setCounter(Integer counter) {
this.counter = counter;
}
public String getCredentialDeviceType() {
return credentialDeviceType;
}
public void setCredentialDeviceType(String credentialDeviceType) {
this.credentialDeviceType = credentialDeviceType;
}
public Boolean getCredentialBackedUp() {
return credentialBackedUp;
}
public void setCredentialBackedUp(Boolean credentialBackedUp) {
this.credentialBackedUp = credentialBackedUp;
}
public String getTransports() {
return transports;
}
public void setTransports(String transports) {
this.transports = transports;
}
}

View File

@@ -1,48 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import org.hibernate.Hibernate;
import java.util.Objects;
@Embeddable
public class AuthenticatorId implements java.io.Serializable {
private static final long serialVersionUID = -4147080603801184737L;
@Column(name = "\"credentialId\"", nullable = false, length = Integer.MAX_VALUE)
private String credentialId;
@Column(name = "\"userId\"", nullable = false, length = Integer.MAX_VALUE)
private String userId;
public String getCredentialId() {
return credentialId;
}
public void setCredentialId(String credentialId) {
this.credentialId = credentialId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
AuthenticatorId entity = (AuthenticatorId) o;
return Objects.equals(this.credentialId, entity.credentialId) &&
Objects.equals(this.userId, entity.userId);
}
@Override
public int hashCode() {
return Objects.hash(credentialId, userId);
}
}

View File

@@ -1,107 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "bal_resellers")
public class BalReseller {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "name", nullable = false, length = 100)
private String name;
@Column(name = "website_url")
private String websiteUrl;
@Column(name = "contact_email", length = 100)
private String contactEmail;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "deleted_at")
private Instant deletedAt;
@ColumnDefault("gen_random_uuid()")
@Column(name = "uuid")
private UUID uuid;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getWebsiteUrl() {
return websiteUrl;
}
public void setWebsiteUrl(String websiteUrl) {
this.websiteUrl = websiteUrl;
}
public String getContactEmail() {
return contactEmail;
}
public void setContactEmail(String contactEmail) {
this.contactEmail = contactEmail;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(Instant deletedAt) {
this.deletedAt = deletedAt;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
}

View File

@@ -1,393 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "bb_products")
public class BbProduct {
@Id
@ColumnDefault("gen_random_uuid()")
@Column(name = "uuid", nullable = false)
private UUID id;
@Column(name = "\"UPC\"", length = 100)
private String upc;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "deleted_at")
private Instant deletedAt;
@Column(name = "\"SKU\"", length = 50)
private String sku;
@Column(name = "\"MANUFACTURER_ID\"", length = 50)
private String manufacturerId;
@Column(name = "\"BRAND_NAME\"", length = 50)
private String brandName;
@Column(name = "\"PRODUCT_NAME\"")
private String productName;
@Column(name = "\"LONG_DESCRIPTION\"", length = Integer.MAX_VALUE)
private String longDescription;
@Column(name = "\"SHORT_DESCRIPTION\"", length = 500)
private String shortDescription;
@Column(name = "\"DEPARTMENT\"", length = 100)
private String department;
@Column(name = "\"CATEGORY\"", length = 100)
private String category;
@Column(name = "\"SUBCATEGORY\"", length = 100)
private String subcategory;
@Column(name = "\"THUMB_URL\"", length = 500)
private String thumbUrl;
@Column(name = "\"IMAGE_URL\"", length = 500)
private String imageUrl;
@Column(name = "\"BUY_LINK\"", length = 500)
private String buyLink;
@Column(name = "\"KEYWORDS\"", length = 500)
private String keywords;
@Column(name = "\"REVIEWS\"", length = 500)
private String reviews;
@Column(name = "\"RETAIL_PRICE\"", length = 50)
private String retailPrice;
@Column(name = "\"SALE_PRICE\"", length = 50)
private String salePrice;
@Column(name = "\"BRAND_PAGE_LINK\"", length = 500)
private String brandPageLink;
@Column(name = "\"BRAND_LOGO_IMAGE\"", length = 500)
private String brandLogoImage;
@Column(name = "\"PRODUCT_PAGE_VIEW_TRACKING\"", length = 500)
private String productPageViewTracking;
@Column(name = "\"PARENT_GROUP_ID\"", length = 200)
private String parentGroupId;
@Column(name = "\"FINELINE\"", length = 200)
private String fineline;
@Column(name = "\"SUPERFINELINE\"", length = 200)
private String superfineline;
@Column(name = "\"MODELNUMBER\"", length = 100)
private String modelnumber;
@Column(name = "\"CALIBER\"", length = 200)
private String caliber;
@Column(name = "\"MEDIUM_IMAGE_URL\"", length = 500)
private String mediumImageUrl;
@Column(name = "\"PRODUCT_CONTENT_WIDGET\"", length = 500)
private String productContentWidget;
@Column(name = "\"GOOGLE_CATEGORIZATION\"", length = 500)
private String googleCategorization;
@Column(name = "\"ITEM_BASED_COMMISSION\"", length = 500)
private String itemBasedCommission;
@Column(name = "\"ITEM_BASED_COMMISSION RATE\"", length = 50)
private String itemBasedCommissionRate;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getUpc() {
return upc;
}
public void setUpc(String upc) {
this.upc = upc;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(Instant deletedAt) {
this.deletedAt = deletedAt;
}
public String getSku() {
return sku;
}
public void setSku(String sku) {
this.sku = sku;
}
public String getManufacturerId() {
return manufacturerId;
}
public void setManufacturerId(String manufacturerId) {
this.manufacturerId = manufacturerId;
}
public String getBrandName() {
return brandName;
}
public void setBrandName(String brandName) {
this.brandName = brandName;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getLongDescription() {
return longDescription;
}
public void setLongDescription(String longDescription) {
this.longDescription = longDescription;
}
public String getShortDescription() {
return shortDescription;
}
public void setShortDescription(String shortDescription) {
this.shortDescription = shortDescription;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getSubcategory() {
return subcategory;
}
public void setSubcategory(String subcategory) {
this.subcategory = subcategory;
}
public String getThumbUrl() {
return thumbUrl;
}
public void setThumbUrl(String thumbUrl) {
this.thumbUrl = thumbUrl;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getBuyLink() {
return buyLink;
}
public void setBuyLink(String buyLink) {
this.buyLink = buyLink;
}
public String getKeywords() {
return keywords;
}
public void setKeywords(String keywords) {
this.keywords = keywords;
}
public String getReviews() {
return reviews;
}
public void setReviews(String reviews) {
this.reviews = reviews;
}
public String getRetailPrice() {
return retailPrice;
}
public void setRetailPrice(String retailPrice) {
this.retailPrice = retailPrice;
}
public String getSalePrice() {
return salePrice;
}
public void setSalePrice(String salePrice) {
this.salePrice = salePrice;
}
public String getBrandPageLink() {
return brandPageLink;
}
public void setBrandPageLink(String brandPageLink) {
this.brandPageLink = brandPageLink;
}
public String getBrandLogoImage() {
return brandLogoImage;
}
public void setBrandLogoImage(String brandLogoImage) {
this.brandLogoImage = brandLogoImage;
}
public String getProductPageViewTracking() {
return productPageViewTracking;
}
public void setProductPageViewTracking(String productPageViewTracking) {
this.productPageViewTracking = productPageViewTracking;
}
public String getParentGroupId() {
return parentGroupId;
}
public void setParentGroupId(String parentGroupId) {
this.parentGroupId = parentGroupId;
}
public String getFineline() {
return fineline;
}
public void setFineline(String fineline) {
this.fineline = fineline;
}
public String getSuperfineline() {
return superfineline;
}
public void setSuperfineline(String superfineline) {
this.superfineline = superfineline;
}
public String getModelnumber() {
return modelnumber;
}
public void setModelnumber(String modelnumber) {
this.modelnumber = modelnumber;
}
public String getCaliber() {
return caliber;
}
public void setCaliber(String caliber) {
this.caliber = caliber;
}
public String getMediumImageUrl() {
return mediumImageUrl;
}
public void setMediumImageUrl(String mediumImageUrl) {
this.mediumImageUrl = mediumImageUrl;
}
public String getProductContentWidget() {
return productContentWidget;
}
public void setProductContentWidget(String productContentWidget) {
this.productContentWidget = productContentWidget;
}
public String getGoogleCategorization() {
return googleCategorization;
}
public void setGoogleCategorization(String googleCategorization) {
this.googleCategorization = googleCategorization;
}
public String getItemBasedCommission() {
return itemBasedCommission;
}
public void setItemBasedCommission(String itemBasedCommission) {
this.itemBasedCommission = itemBasedCommission;
}
public String getItemBasedCommissionRate() {
return itemBasedCommissionRate;
}
public void setItemBasedCommissionRate(String itemBasedCommissionRate) {
this.itemBasedCommissionRate = itemBasedCommissionRate;
}
}

View File

@@ -1,10 +1,8 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.Instant;
import java.util.UUID;
@@ -12,39 +10,48 @@ import java.util.UUID;
@Entity
@Table(name = "brands")
public class Brand {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "name", nullable = false, length = 100)
@Column(nullable = false, unique = true)
private String name;
@ColumnDefault("now()")
@Column(nullable = false, unique = true)
private UUID uuid;
@Column(unique = true)
private String slug;
@Column(name = "website")
private String website;
@Column(name = "logo_url")
private String logoUrl;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "deleted_at")
private Instant deletedAt;
@ColumnDefault("gen_random_uuid()")
@Column(name = "uuid")
private UUID uuid;
@Column(name = "url", length = Integer.MAX_VALUE)
private String url;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
@PrePersist
public void prePersist() {
if (uuid == null) {
uuid = UUID.randomUUID();
}
if (slug == null && name != null) {
slug = name.toLowerCase().replace(" ", "-");
}
}
// Getters and Setters
public Integer getId() {
return id;
}
@@ -61,12 +68,36 @@ public class Brand {
this.name = name;
}
public Instant getUpdatedAt() {
return updatedAt;
public UUID getUuid() {
return uuid;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
public String getSlug() {
return slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
public String getWebsite() {
return website;
}
public void setWebsite(String website) {
this.website = website;
}
public String getLogoUrl() {
return logoUrl;
}
public void setLogoUrl(String logoUrl) {
this.logoUrl = logoUrl;
}
public Instant getCreatedAt() {
@@ -77,6 +108,14 @@ public class Brand {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
public Instant getDeletedAt() {
return deletedAt;
}
@@ -84,13 +123,4 @@ public class Brand {
public void setDeletedAt(Instant deletedAt) {
this.deletedAt = deletedAt;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
}

View File

@@ -0,0 +1,143 @@
package group.goforward.ballistic.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@Table(name = "build_items")
public class BuildItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Integer id;
@NotNull
@ColumnDefault("gen_random_uuid()")
@Column(name = "uuid", nullable = false)
private UUID uuid;
@NotNull
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "build_id", nullable = false)
private Build build;
@NotNull
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@NotNull
@Column(name = "slot", nullable = false, length = Integer.MAX_VALUE)
private String slot;
@NotNull
@ColumnDefault("0")
@Column(name = "\"position\"", nullable = false)
private Integer position;
@NotNull
@ColumnDefault("1")
@Column(name = "quantity", nullable = false)
private Integer quantity;
@NotNull
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "updated_at")
private OffsetDateTime updatedAt;
@Column(name = "deleted_at")
private OffsetDateTime deletedAt;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
public Build getBuild() {
return build;
}
public void setBuild(Build build) {
this.build = build;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
public String getSlot() {
return slot;
}
public void setSlot(String slot) {
this.slot = slot;
}
public Integer getPosition() {
return position;
}
public void setPosition(Integer position) {
this.position = position;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public OffsetDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(OffsetDateTime deletedAt) {
this.deletedAt = deletedAt;
}
}

View File

@@ -1,96 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "categories")
public class Category {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "name", nullable = false, length = 100)
private String name;
@Column(name = "parent_category_id")
private Integer parentCategoryId;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "deleted_at")
private Instant deletedAt;
@ColumnDefault("gen_random_uuid()")
@Column(name = "uuid")
private UUID uuid;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getParentCategoryId() {
return parentCategoryId;
}
public void setParentCategoryId(Integer parentCategoryId) {
this.parentCategoryId = parentCategoryId;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(Instant deletedAt) {
this.deletedAt = deletedAt;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
}

View File

@@ -0,0 +1,98 @@
// src/main/java/group/goforward/ballistic/model/CategoryMapping.java
package group.goforward.ballistic.model;
import jakarta.persistence.*;
import java.time.OffsetDateTime;
@Entity
@Table(name = "category_mappings")
public class CategoryMapping {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Integer id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "merchant_id", nullable = false)
private Merchant merchant;
@Column(name = "raw_category_path", nullable = false)
private String rawCategoryPath;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "part_category_id")
private PartCategory partCategory;
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt = OffsetDateTime.now();
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt = OffsetDateTime.now();
@PrePersist
public void onCreate() {
OffsetDateTime now = OffsetDateTime.now();
if (createdAt == null) {
createdAt = now;
}
if (updatedAt == null) {
updatedAt = now;
}
}
@PreUpdate
public void onUpdate() {
this.updatedAt = OffsetDateTime.now();
}
// --- getters & setters ---
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Merchant getMerchant() {
return merchant;
}
public void setMerchant(Merchant merchant) {
this.merchant = merchant;
}
public String getRawCategoryPath() {
return rawCategoryPath;
}
public void setRawCategoryPath(String rawCategoryPath) {
this.rawCategoryPath = rawCategoryPath;
}
public PartCategory getPartCategory() {
return partCategory;
}
public void setPartCategory(PartCategory partCategory) {
this.partCategory = partCategory;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -1,85 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "compartment")
public class Compartment {
@Id
@ColumnDefault("gen_random_uuid()")
@Column(name = "id", nullable = false)
private UUID id;
@Column(name = "name", nullable = false, length = 100)
private String name;
@Column(name = "description", length = 300)
private String description;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "deleted_at")
private Instant deletedAt;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(Instant deletedAt) {
this.deletedAt = deletedAt;
}
}

View File

@@ -1,71 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import java.time.OffsetDateTime;
@Entity
@Table(name = "email_verification_codes")
public class EmailVerificationCode {
@Id
@ColumnDefault("nextval('email_verification_codes_id_seq')")
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "user_id", nullable = false, length = 21)
private String userId;
@Column(name = "email", nullable = false)
private String email;
@Column(name = "code", nullable = false, length = 8)
private String code;
@Column(name = "expires_at", nullable = false)
private OffsetDateTime expiresAt;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public OffsetDateTime getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(OffsetDateTime expiresAt) {
this.expiresAt = expiresAt;
}
}

View File

@@ -1,28 +1,27 @@
package group.goforward.ballistic.model;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import java.time.OffsetDateTime;
import java.time.Instant;
@Entity
@Table(name = "feed_imports")
public class FeedImport {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
// merchant_id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "merchant_id", nullable = false)
private Merchant merchant;
@ColumnDefault("now()")
@Column(name = "started_at", nullable = false)
private OffsetDateTime startedAt;
private Instant startedAt;
@Column(name = "finished_at")
private OffsetDateTime finishedAt;
private Instant finishedAt;
@Column(name = "rows_total")
private Integer rowsTotal;
@@ -36,21 +35,18 @@ public class FeedImport {
@Column(name = "rows_updated")
private Integer rowsUpdated;
@ColumnDefault("'running'")
@Column(name = "status", nullable = false, length = Integer.MAX_VALUE)
private String status;
@Column(name = "status", nullable = false)
private String status = "running";
@Column(name = "error_message", length = Integer.MAX_VALUE)
@Column(name = "error_message")
private String errorMessage;
// getters & setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Merchant getMerchant() {
return merchant;
}
@@ -59,19 +55,19 @@ public class FeedImport {
this.merchant = merchant;
}
public OffsetDateTime getStartedAt() {
public Instant getStartedAt() {
return startedAt;
}
public void setStartedAt(OffsetDateTime startedAt) {
public void setStartedAt(Instant startedAt) {
this.startedAt = startedAt;
}
public OffsetDateTime getFinishedAt() {
public Instant getFinishedAt() {
return finishedAt;
}
public void setFinishedAt(OffsetDateTime finishedAt) {
public void setFinishedAt(Instant finishedAt) {
this.finishedAt = finishedAt;
}
@@ -122,5 +118,4 @@ public class FeedImport {
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}

View File

@@ -1,909 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import java.time.Instant;
@Entity
@Table(name = "lipseycatalog")
public class Lipseycatalog {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "itemno", nullable = false, length = 20)
private String itemno;
@Column(name = "description1", length = Integer.MAX_VALUE)
private String description1;
@Column(name = "description2", length = Integer.MAX_VALUE)
private String description2;
@Column(name = "upc", length = 20)
private String upc;
@Column(name = "manufacturermodelno", length = 30)
private String manufacturermodelno;
@Column(name = "msrp")
private Double msrp;
@Column(name = "model", length = Integer.MAX_VALUE)
private String model;
@Column(name = "calibergauge", length = Integer.MAX_VALUE)
private String calibergauge;
@Column(name = "manufacturer", length = Integer.MAX_VALUE)
private String manufacturer;
@Column(name = "type", length = Integer.MAX_VALUE)
private String type;
@Column(name = "action", length = Integer.MAX_VALUE)
private String action;
@Column(name = "barrellength", length = Integer.MAX_VALUE)
private String barrellength;
@Column(name = "capacity", length = Integer.MAX_VALUE)
private String capacity;
@Column(name = "finish", length = Integer.MAX_VALUE)
private String finish;
@Column(name = "overalllength", length = Integer.MAX_VALUE)
private String overalllength;
@Column(name = "receiver", length = Integer.MAX_VALUE)
private String receiver;
@Column(name = "safety", length = Integer.MAX_VALUE)
private String safety;
@Column(name = "sights", length = Integer.MAX_VALUE)
private String sights;
@Column(name = "stockframegrips", length = Integer.MAX_VALUE)
private String stockframegrips;
@Column(name = "magazine", length = Integer.MAX_VALUE)
private String magazine;
@Column(name = "weight", length = Integer.MAX_VALUE)
private String weight;
@Column(name = "imagename", length = Integer.MAX_VALUE)
private String imagename;
@Column(name = "chamber", length = Integer.MAX_VALUE)
private String chamber;
@Column(name = "drilledandtapped", length = Integer.MAX_VALUE)
private String drilledandtapped;
@Column(name = "rateoftwist", length = Integer.MAX_VALUE)
private String rateoftwist;
@Column(name = "itemtype", length = Integer.MAX_VALUE)
private String itemtype;
@Column(name = "additionalfeature1", length = Integer.MAX_VALUE)
private String additionalfeature1;
@Column(name = "additionalfeature2", length = Integer.MAX_VALUE)
private String additionalfeature2;
@Column(name = "additionalfeature3", length = Integer.MAX_VALUE)
private String additionalfeature3;
@Column(name = "shippingweight", length = Integer.MAX_VALUE)
private String shippingweight;
@Column(name = "boundbookmanufacturer", length = Integer.MAX_VALUE)
private String boundbookmanufacturer;
@Column(name = "boundbookmodel", length = Integer.MAX_VALUE)
private String boundbookmodel;
@Column(name = "boundbooktype", length = Integer.MAX_VALUE)
private String boundbooktype;
@Column(name = "nfathreadpattern", length = Integer.MAX_VALUE)
private String nfathreadpattern;
@Column(name = "nfaattachmentmethod", length = Integer.MAX_VALUE)
private String nfaattachmentmethod;
@Column(name = "nfabaffletype", length = Integer.MAX_VALUE)
private String nfabaffletype;
@Column(name = "silencercanbedisassembled", length = Integer.MAX_VALUE)
private String silencercanbedisassembled;
@Column(name = "silencerconstructionmaterial", length = Integer.MAX_VALUE)
private String silencerconstructionmaterial;
@Column(name = "nfadbreduction", length = Integer.MAX_VALUE)
private String nfadbreduction;
@Column(name = "silenceroutsidediameter", length = Integer.MAX_VALUE)
private String silenceroutsidediameter;
@Column(name = "\"nfaform3Caliber\"", length = Integer.MAX_VALUE)
private String nfaform3Caliber;
@Column(name = "opticmagnification", length = Integer.MAX_VALUE)
private String opticmagnification;
@Column(name = "maintubesize", length = Integer.MAX_VALUE)
private String maintubesize;
@Column(name = "adjustableobjective", length = Integer.MAX_VALUE)
private String adjustableobjective;
@Column(name = "objectivesize", length = Integer.MAX_VALUE)
private String objectivesize;
@Column(name = "opticadjustments", length = Integer.MAX_VALUE)
private String opticadjustments;
@Column(name = "illuminatedreticle", length = Integer.MAX_VALUE)
private String illuminatedreticle;
@Column(name = "reticle", length = Integer.MAX_VALUE)
private String reticle;
@Column(name = "exclusive", length = Integer.MAX_VALUE)
private String exclusive;
@ColumnDefault("NULL")
@Column(name = "quantity", length = 10)
private String quantity;
@Column(name = "allocated", length = Integer.MAX_VALUE)
private String allocated;
@Column(name = "onsale", length = Integer.MAX_VALUE)
private String onsale;
@Column(name = "price")
private Double price;
@Column(name = "currentprice")
private Double currentprice;
@Column(name = "retailmap")
private Double retailmap;
@Column(name = "fflrequired", length = Integer.MAX_VALUE)
private String fflrequired;
@Column(name = "sotrequired", length = Integer.MAX_VALUE)
private String sotrequired;
@Column(name = "exclusivetype", length = Integer.MAX_VALUE)
private String exclusivetype;
@Column(name = "scopecoverincluded", length = Integer.MAX_VALUE)
private String scopecoverincluded;
@Column(name = "special", length = Integer.MAX_VALUE)
private String special;
@Column(name = "sightstype", length = Integer.MAX_VALUE)
private String sightstype;
@Column(name = "\"case\"", length = Integer.MAX_VALUE)
private String caseField;
@Column(name = "choke", length = Integer.MAX_VALUE)
private String choke;
@Column(name = "dbreduction", length = Integer.MAX_VALUE)
private String dbreduction;
@Column(name = "family", length = Integer.MAX_VALUE)
private String family;
@Column(name = "finishtype", length = Integer.MAX_VALUE)
private String finishtype;
@Column(name = "frame", length = Integer.MAX_VALUE)
private String frame;
@Column(name = "griptype", length = 30)
private String griptype;
@Column(name = "handgunslidematerial", length = Integer.MAX_VALUE)
private String handgunslidematerial;
@Column(name = "countryoforigin", length = 4)
private String countryoforigin;
@Column(name = "itemlength", length = Integer.MAX_VALUE)
private String itemlength;
@Column(name = "itemwidth", length = Integer.MAX_VALUE)
private String itemwidth;
@Column(name = "itemheight", length = Integer.MAX_VALUE)
private String itemheight;
@Column(name = "packagelength")
private Double packagelength;
@Column(name = "packagewidth")
private Double packagewidth;
@Column(name = "packageheight")
private Double packageheight;
@Column(name = "itemgroup", length = 40)
private String itemgroup;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "deleted_at")
private Instant deletedAt;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getItemno() {
return itemno;
}
public void setItemno(String itemno) {
this.itemno = itemno;
}
public String getDescription1() {
return description1;
}
public void setDescription1(String description1) {
this.description1 = description1;
}
public String getDescription2() {
return description2;
}
public void setDescription2(String description2) {
this.description2 = description2;
}
public String getUpc() {
return upc;
}
public void setUpc(String upc) {
this.upc = upc;
}
public String getManufacturermodelno() {
return manufacturermodelno;
}
public void setManufacturermodelno(String manufacturermodelno) {
this.manufacturermodelno = manufacturermodelno;
}
public Double getMsrp() {
return msrp;
}
public void setMsrp(Double msrp) {
this.msrp = msrp;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getCalibergauge() {
return calibergauge;
}
public void setCalibergauge(String calibergauge) {
this.calibergauge = calibergauge;
}
public String getManufacturer() {
return manufacturer;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getBarrellength() {
return barrellength;
}
public void setBarrellength(String barrellength) {
this.barrellength = barrellength;
}
public String getCapacity() {
return capacity;
}
public void setCapacity(String capacity) {
this.capacity = capacity;
}
public String getFinish() {
return finish;
}
public void setFinish(String finish) {
this.finish = finish;
}
public String getOveralllength() {
return overalllength;
}
public void setOveralllength(String overalllength) {
this.overalllength = overalllength;
}
public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getSafety() {
return safety;
}
public void setSafety(String safety) {
this.safety = safety;
}
public String getSights() {
return sights;
}
public void setSights(String sights) {
this.sights = sights;
}
public String getStockframegrips() {
return stockframegrips;
}
public void setStockframegrips(String stockframegrips) {
this.stockframegrips = stockframegrips;
}
public String getMagazine() {
return magazine;
}
public void setMagazine(String magazine) {
this.magazine = magazine;
}
public String getWeight() {
return weight;
}
public void setWeight(String weight) {
this.weight = weight;
}
public String getImagename() {
return imagename;
}
public void setImagename(String imagename) {
this.imagename = imagename;
}
public String getChamber() {
return chamber;
}
public void setChamber(String chamber) {
this.chamber = chamber;
}
public String getDrilledandtapped() {
return drilledandtapped;
}
public void setDrilledandtapped(String drilledandtapped) {
this.drilledandtapped = drilledandtapped;
}
public String getRateoftwist() {
return rateoftwist;
}
public void setRateoftwist(String rateoftwist) {
this.rateoftwist = rateoftwist;
}
public String getItemtype() {
return itemtype;
}
public void setItemtype(String itemtype) {
this.itemtype = itemtype;
}
public String getAdditionalfeature1() {
return additionalfeature1;
}
public void setAdditionalfeature1(String additionalfeature1) {
this.additionalfeature1 = additionalfeature1;
}
public String getAdditionalfeature2() {
return additionalfeature2;
}
public void setAdditionalfeature2(String additionalfeature2) {
this.additionalfeature2 = additionalfeature2;
}
public String getAdditionalfeature3() {
return additionalfeature3;
}
public void setAdditionalfeature3(String additionalfeature3) {
this.additionalfeature3 = additionalfeature3;
}
public String getShippingweight() {
return shippingweight;
}
public void setShippingweight(String shippingweight) {
this.shippingweight = shippingweight;
}
public String getBoundbookmanufacturer() {
return boundbookmanufacturer;
}
public void setBoundbookmanufacturer(String boundbookmanufacturer) {
this.boundbookmanufacturer = boundbookmanufacturer;
}
public String getBoundbookmodel() {
return boundbookmodel;
}
public void setBoundbookmodel(String boundbookmodel) {
this.boundbookmodel = boundbookmodel;
}
public String getBoundbooktype() {
return boundbooktype;
}
public void setBoundbooktype(String boundbooktype) {
this.boundbooktype = boundbooktype;
}
public String getNfathreadpattern() {
return nfathreadpattern;
}
public void setNfathreadpattern(String nfathreadpattern) {
this.nfathreadpattern = nfathreadpattern;
}
public String getNfaattachmentmethod() {
return nfaattachmentmethod;
}
public void setNfaattachmentmethod(String nfaattachmentmethod) {
this.nfaattachmentmethod = nfaattachmentmethod;
}
public String getNfabaffletype() {
return nfabaffletype;
}
public void setNfabaffletype(String nfabaffletype) {
this.nfabaffletype = nfabaffletype;
}
public String getSilencercanbedisassembled() {
return silencercanbedisassembled;
}
public void setSilencercanbedisassembled(String silencercanbedisassembled) {
this.silencercanbedisassembled = silencercanbedisassembled;
}
public String getSilencerconstructionmaterial() {
return silencerconstructionmaterial;
}
public void setSilencerconstructionmaterial(String silencerconstructionmaterial) {
this.silencerconstructionmaterial = silencerconstructionmaterial;
}
public String getNfadbreduction() {
return nfadbreduction;
}
public void setNfadbreduction(String nfadbreduction) {
this.nfadbreduction = nfadbreduction;
}
public String getSilenceroutsidediameter() {
return silenceroutsidediameter;
}
public void setSilenceroutsidediameter(String silenceroutsidediameter) {
this.silenceroutsidediameter = silenceroutsidediameter;
}
public String getNfaform3Caliber() {
return nfaform3Caliber;
}
public void setNfaform3Caliber(String nfaform3Caliber) {
this.nfaform3Caliber = nfaform3Caliber;
}
public String getOpticmagnification() {
return opticmagnification;
}
public void setOpticmagnification(String opticmagnification) {
this.opticmagnification = opticmagnification;
}
public String getMaintubesize() {
return maintubesize;
}
public void setMaintubesize(String maintubesize) {
this.maintubesize = maintubesize;
}
public String getAdjustableobjective() {
return adjustableobjective;
}
public void setAdjustableobjective(String adjustableobjective) {
this.adjustableobjective = adjustableobjective;
}
public String getObjectivesize() {
return objectivesize;
}
public void setObjectivesize(String objectivesize) {
this.objectivesize = objectivesize;
}
public String getOpticadjustments() {
return opticadjustments;
}
public void setOpticadjustments(String opticadjustments) {
this.opticadjustments = opticadjustments;
}
public String getIlluminatedreticle() {
return illuminatedreticle;
}
public void setIlluminatedreticle(String illuminatedreticle) {
this.illuminatedreticle = illuminatedreticle;
}
public String getReticle() {
return reticle;
}
public void setReticle(String reticle) {
this.reticle = reticle;
}
public String getExclusive() {
return exclusive;
}
public void setExclusive(String exclusive) {
this.exclusive = exclusive;
}
public String getQuantity() {
return quantity;
}
public void setQuantity(String quantity) {
this.quantity = quantity;
}
public String getAllocated() {
return allocated;
}
public void setAllocated(String allocated) {
this.allocated = allocated;
}
public String getOnsale() {
return onsale;
}
public void setOnsale(String onsale) {
this.onsale = onsale;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Double getCurrentprice() {
return currentprice;
}
public void setCurrentprice(Double currentprice) {
this.currentprice = currentprice;
}
public Double getRetailmap() {
return retailmap;
}
public void setRetailmap(Double retailmap) {
this.retailmap = retailmap;
}
public String getFflrequired() {
return fflrequired;
}
public void setFflrequired(String fflrequired) {
this.fflrequired = fflrequired;
}
public String getSotrequired() {
return sotrequired;
}
public void setSotrequired(String sotrequired) {
this.sotrequired = sotrequired;
}
public String getExclusivetype() {
return exclusivetype;
}
public void setExclusivetype(String exclusivetype) {
this.exclusivetype = exclusivetype;
}
public String getScopecoverincluded() {
return scopecoverincluded;
}
public void setScopecoverincluded(String scopecoverincluded) {
this.scopecoverincluded = scopecoverincluded;
}
public String getSpecial() {
return special;
}
public void setSpecial(String special) {
this.special = special;
}
public String getSightstype() {
return sightstype;
}
public void setSightstype(String sightstype) {
this.sightstype = sightstype;
}
public String getCaseField() {
return caseField;
}
public void setCaseField(String caseField) {
this.caseField = caseField;
}
public String getChoke() {
return choke;
}
public void setChoke(String choke) {
this.choke = choke;
}
public String getDbreduction() {
return dbreduction;
}
public void setDbreduction(String dbreduction) {
this.dbreduction = dbreduction;
}
public String getFamily() {
return family;
}
public void setFamily(String family) {
this.family = family;
}
public String getFinishtype() {
return finishtype;
}
public void setFinishtype(String finishtype) {
this.finishtype = finishtype;
}
public String getFrame() {
return frame;
}
public void setFrame(String frame) {
this.frame = frame;
}
public String getGriptype() {
return griptype;
}
public void setGriptype(String griptype) {
this.griptype = griptype;
}
public String getHandgunslidematerial() {
return handgunslidematerial;
}
public void setHandgunslidematerial(String handgunslidematerial) {
this.handgunslidematerial = handgunslidematerial;
}
public String getCountryoforigin() {
return countryoforigin;
}
public void setCountryoforigin(String countryoforigin) {
this.countryoforigin = countryoforigin;
}
public String getItemlength() {
return itemlength;
}
public void setItemlength(String itemlength) {
this.itemlength = itemlength;
}
public String getItemwidth() {
return itemwidth;
}
public void setItemwidth(String itemwidth) {
this.itemwidth = itemwidth;
}
public String getItemheight() {
return itemheight;
}
public void setItemheight(String itemheight) {
this.itemheight = itemheight;
}
public Double getPackagelength() {
return packagelength;
}
public void setPackagelength(Double packagelength) {
this.packagelength = packagelength;
}
public Double getPackagewidth() {
return packagewidth;
}
public void setPackagewidth(Double packagewidth) {
this.packagewidth = packagewidth;
}
public Double getPackageheight() {
return packageheight;
}
public void setPackageheight(Double packageheight) {
this.packageheight = packageheight;
}
public String getItemgroup() {
return itemgroup;
}
public void setItemgroup(String itemgroup) {
this.itemgroup = itemgroup;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(Instant deletedAt) {
this.deletedAt = deletedAt;
}
}

View File

@@ -1,85 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "manufacturer")
public class Manufacturer {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "name", nullable = false, length = 100)
private String name;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "deleted_at")
private Instant deletedAt;
@ColumnDefault("gen_random_uuid()")
@Column(name = "uuid")
private UUID uuid;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(Instant deletedAt) {
this.deletedAt = deletedAt;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
}

View File

@@ -8,6 +8,7 @@ import java.time.OffsetDateTime;
@Entity
@Table(name = "merchants")
public class Merchant {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
@@ -22,9 +23,18 @@ public class Merchant {
@Column(name = "feed_url", nullable = false, length = Integer.MAX_VALUE)
private String feedUrl;
@Column(name = "offer_feed_url")
private String offerFeedUrl;
@Column(name = "last_full_import_at")
private OffsetDateTime lastFullImportAt;
@Column(name = "last_offer_sync_at")
private OffsetDateTime lastOfferSyncAt;
@ColumnDefault("true")
@Column(name = "is_active", nullable = false)
private Boolean isActive = false;
private Boolean isActive = true;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
@@ -34,6 +44,10 @@ public class Merchant {
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
// -----------------------
// GETTERS & SETTERS
// -----------------------
public Integer getId() {
return id;
}
@@ -66,12 +80,36 @@ public class Merchant {
this.feedUrl = feedUrl;
}
public String getOfferFeedUrl() {
return offerFeedUrl;
}
public void setOfferFeedUrl(String offerFeedUrl) {
this.offerFeedUrl = offerFeedUrl;
}
public OffsetDateTime getLastFullImportAt() {
return lastFullImportAt;
}
public void setLastFullImportAt(OffsetDateTime lastFullImportAt) {
this.lastFullImportAt = lastFullImportAt;
}
public OffsetDateTime getLastOfferSyncAt() {
return lastOfferSyncAt;
}
public void setLastOfferSyncAt(OffsetDateTime lastOfferSyncAt) {
this.lastOfferSyncAt = lastOfferSyncAt;
}
public Boolean getIsActive() {
return isActive;
}
public void setIsActive(Boolean isActive) {
this.isActive = isActive;
public void setIsActive(Boolean active) {
this.isActive = active;
}
public OffsetDateTime getCreatedAt() {
@@ -89,5 +127,4 @@ public class Merchant {
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,174 @@
package group.goforward.ballistic.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
@Entity
@Table(name = "merchant_category_map")
public class MerchantCategoryMap {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Integer id;
@NotNull
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "merchant_id", nullable = false)
private Merchant merchant;
@NotNull
@Column(name = "raw_category", nullable = false, length = Integer.MAX_VALUE)
private String rawCategory;
@Column(name = "canonical_part_role", length = Integer.MAX_VALUE)
private String canonicalPartRole;
@Column(name = "confidence", precision = 5, scale = 2)
private BigDecimal confidence;
@Column(name = "notes", length = Integer.MAX_VALUE)
private String notes;
@NotNull
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@NotNull
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
@Size(max = 255)
@Column(name = "canonical_category")
private String canonicalCategory;
@NotNull
@ColumnDefault("true")
@Column(name = "enabled", nullable = false)
private Boolean enabled = false;
@Size(max = 100)
@Column(name = "platform", length = 100)
private String platform;
@Size(max = 100)
@Column(name = "part_role", length = 100)
private String partRole;
@Column(name = "deleted_at")
private OffsetDateTime deletedAt;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Merchant getMerchant() {
return merchant;
}
public void setMerchant(Merchant merchant) {
this.merchant = merchant;
}
public String getRawCategory() {
return rawCategory;
}
public void setRawCategory(String rawCategory) {
this.rawCategory = rawCategory;
}
public String getCanonicalPartRole() {
return canonicalPartRole;
}
public void setCanonicalPartRole(String canonicalPartRole) {
this.canonicalPartRole = canonicalPartRole;
}
public BigDecimal getConfidence() {
return confidence;
}
public void setConfidence(BigDecimal confidence) {
this.confidence = confidence;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public String getCanonicalCategory() {
return canonicalCategory;
}
public void setCanonicalCategory(String canonicalCategory) {
this.canonicalCategory = canonicalCategory;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getPartRole() {
return partRole;
}
public void setPartRole(String partRole) {
this.partRole = partRole;
}
public OffsetDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(OffsetDateTime deletedAt) {
this.deletedAt = deletedAt;
}
}

View File

@@ -0,0 +1,105 @@
package group.goforward.ballistic.model;
import jakarta.persistence.*;
import java.time.OffsetDateTime;
import group.goforward.ballistic.model.ProductConfiguration;
@Entity
@Table(
name = "merchant_category_mappings",
uniqueConstraints = @UniqueConstraint(
name = "uq_merchant_category",
columnNames = { "merchant_id", "raw_category" }
)
)
public class MerchantCategoryMapping {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // SERIAL
@Column(name = "id", nullable = false)
private Integer id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "merchant_id", nullable = false)
private Merchant merchant;
@Column(name = "raw_category", nullable = false, length = 512)
private String rawCategory;
@Column(name = "mapped_part_role", length = 128)
private String mappedPartRole; // e.g. "upper-receiver", "barrel"
@Column(name = "mapped_configuration")
@Enumerated(EnumType.STRING)
private ProductConfiguration mappedConfiguration;
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt = OffsetDateTime.now();
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt = OffsetDateTime.now();
@PreUpdate
public void onUpdate() {
this.updatedAt = OffsetDateTime.now();
}
// getters & setters
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Merchant getMerchant() {
return merchant;
}
public void setMerchant(Merchant merchant) {
this.merchant = merchant;
}
public String getRawCategory() {
return rawCategory;
}
public void setRawCategory(String rawCategory) {
this.rawCategory = rawCategory;
}
public String getMappedPartRole() {
return mappedPartRole;
}
public void setMappedPartRole(String mappedPartRole) {
this.mappedPartRole = mappedPartRole;
}
public ProductConfiguration getMappedConfiguration() {
return mappedConfiguration;
}
public void setMappedConfiguration(ProductConfiguration mappedConfiguration) {
this.mappedConfiguration = mappedConfiguration;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -1,24 +1,49 @@
package group.goforward.ballistic.model;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@Table(name = "part_categories")
public class PartCategory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "slug", nullable = false, length = Integer.MAX_VALUE)
@Column(name = "slug", nullable = false, unique = true)
private String slug;
@Column(name = "name", nullable = false, length = Integer.MAX_VALUE)
@Column(name = "name", nullable = false)
private String name;
@Column(name = "description", length = Integer.MAX_VALUE)
@Column(name = "description")
private String description;
@ColumnDefault("gen_random_uuid()")
@Column(name = "uuid", nullable = false)
private UUID uuid;
@Column(name = "group_name")
private String groupName;
@Column(name = "sort_order")
private Integer sortOrder;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
// --- Getters & Setters ---
public Integer getId() {
return id;
}
@@ -51,4 +76,43 @@ public class PartCategory {
this.description = description;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
public Integer getSortOrder() {
return sortOrder;
}
public void setSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,56 @@
package group.goforward.ballistic.model;
import jakarta.persistence.*;
import java.time.OffsetDateTime;
@Entity
@Table(name = "part_role_category_mappings",
uniqueConstraints = @UniqueConstraint(columnNames = {"platform", "part_role"}))
public class PartRoleCategoryMapping {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "platform", nullable = false)
private String platform;
@Column(name = "part_role", nullable = false)
private String partRole;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_slug", referencedColumnName = "slug", nullable = false)
private PartCategory category;
@Column(name = "notes")
private String notes;
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
// getters/setters…
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getPlatform() { return platform; }
public void setPlatform(String platform) { this.platform = platform; }
public String getPartRole() { return partRole; }
public void setPartRole(String partRole) { this.partRole = partRole; }
public PartCategory getCategory() { return category; }
public void setCategory(PartCategory category) { this.category = category; }
public String getNotes() { return notes; }
public void setNotes(String notes) { this.notes = notes; }
public OffsetDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
public OffsetDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
}

View File

@@ -0,0 +1,65 @@
package group.goforward.ballistic.model;
import jakarta.persistence.*;
@Entity
@Table(name = "part_role_mappings")
public class PartRoleMapping {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false)
private String platform; // e.g. "AR-15"
@Column(name = "part_role", nullable = false)
private String partRole; // e.g. "UPPER", "BARREL", etc.
@ManyToOne(optional = false)
@JoinColumn(name = "part_category_id")
private PartCategory partCategory;
@Column(columnDefinition = "text")
private String notes;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getPartRole() {
return partRole;
}
public void setPartRole(String partRole) {
this.partRole = partRole;
}
public PartCategory getPartCategory() {
return partCategory;
}
public void setPartCategory(PartCategory partCategory) {
this.partCategory = partCategory;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
}

View File

@@ -1,47 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
@Entity
@Table(name = "password_reset_tokens")
public class PasswordResetToken {
@Id
@Column(name = "id", nullable = false, length = 40)
private String id;
@Column(name = "user_id", nullable = false, length = 21)
private String userId;
@Column(name = "expires_at", nullable = false)
private OffsetDateTime expiresAt;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public OffsetDateTime getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(OffsetDateTime expiresAt) {
this.expiresAt = expiresAt;
}
}

View File

@@ -0,0 +1,84 @@
package group.goforward.ballistic.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import org.hibernate.annotations.ColumnDefault;
import java.time.OffsetDateTime;
@Entity
@Table(name = "platforms")
public class Platform {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Integer id;
@NotNull
@Column(name = "key", nullable = false, length = Integer.MAX_VALUE)
private String key;
@NotNull
@Column(name = "label", nullable = false, length = Integer.MAX_VALUE)
private String label;
@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "created_at")
private OffsetDateTime createdAt;
@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "updated_at")
private OffsetDateTime updatedAt;
@Column(name = "deleted_at")
private OffsetDateTime deletedAt;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
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 OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public OffsetDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(OffsetDateTime deletedAt) {
this.deletedAt = deletedAt;
}
}

View File

@@ -1,116 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import java.time.Instant;
@Entity
@Table(name = "posts")
public class Post {
@Id
@Column(name = "id", nullable = false, length = 15)
private String id;
@Column(name = "user_id", nullable = false)
private String userId;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "excerpt", nullable = false)
private String excerpt;
@Column(name = "content", nullable = false, length = Integer.MAX_VALUE)
private String content;
@ColumnDefault("'draft'")
@Column(name = "status", nullable = false, length = 10)
private String status;
@Column(name = "tags")
private String tags;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "updated_at")
private Instant updatedAt;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getExcerpt() {
return excerpt;
}
public void setExcerpt(String excerpt) {
this.excerpt = excerpt;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getTags() {
return tags;
}
public void setTags(String tags) {
this.tags = tags;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -1,41 +1,35 @@
package group.goforward.ballistic.model;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.time.Instant;
@Entity
@Table(name = "price_history")
public class PriceHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@OnDelete(action = OnDeleteAction.CASCADE)
// product_offer_id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_offer_id", nullable = false)
private ProductOffer productOffer;
@Column(name = "price", nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@ColumnDefault("now()")
@Column(name = "recorded_at", nullable = false)
private OffsetDateTime recordedAt;
private Instant recordedAt;
// getters & setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public ProductOffer getProductOffer() {
return productOffer;
}
@@ -52,12 +46,11 @@ public class PriceHistory {
this.price = price;
}
public OffsetDateTime getRecordedAt() {
public Instant getRecordedAt() {
return recordedAt;
}
public void setRecordedAt(OffsetDateTime recordedAt) {
public void setRecordedAt(Instant recordedAt) {
this.recordedAt = recordedAt;
}
}

View File

@@ -1,185 +1,154 @@
package group.goforward.ballistic.model;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.UUID;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.Objects;
import java.util.Set;
import java.util.HashSet;
import group.goforward.ballistic.model.ProductOffer;
import group.goforward.ballistic.model.ProductConfiguration;
@Entity
@Table(name = "products")
@NamedQuery(name="Products.findByPlatformWithBrand", query= "" +
"SELECT p FROM Product p" +
" JOIN FETCH p.brand b" +
" WHERE p.platform = :platform" +
" AND p.deletedAt IS NULL")
@NamedQuery(name="Product.findByPlatformAndPartRoleInWithBrand", query= "" +
"SELECT p FROM Product p JOIN FETCH p.brand b" +
" WHERE p.platform = :platform" +
" AND p.partRole IN :roles" +
" AND p.deletedAt IS NULL")
@NamedQuery(name="Product.findProductsbyBrandByOffers", query="" +
" SELECT DISTINCT p FROM Product p" +
" LEFT JOIN FETCH p.brand b" +
" LEFT JOIN FETCH p.offers o" +
" WHERE p.platform = :platform" +
" AND p.deletedAt IS NULL")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // uses products_id_seq in Postgres
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "uuid", nullable = false, updatable = false)
private UUID uuid;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "brand_id", nullable = false)
private Brand brand;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "description", nullable = false, length = Integer.MAX_VALUE)
@Column(name = "slug", nullable = false)
private String slug;
@Column(name = "mpn")
private String mpn;
@Column(name = "upc")
private String upc;
@Column(name = "platform")
private String platform;
@Column(name = "part_role")
private String partRole;
@Column(name = "configuration")
@Enumerated(EnumType.STRING)
private ProductConfiguration configuration;
@Column(name = "short_description")
private String shortDescription;
@Column(name = "description")
private String description;
@Column(name = "price", nullable = false)
private BigDecimal price;
@Column(name = "main_image_url")
private String mainImageUrl;
@Column(name = "reseller_id", nullable = false)
private Integer resellerId;
@Column(name = "category_id", nullable = false)
private Integer categoryId;
@ColumnDefault("0")
@Column(name = "stock_qty")
private Integer stockQty;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "updated_at")
private Instant updatedAt;
@Column(name = "deleted_at")
private Instant deletedAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "brand_id")
private Brand brand;
@Column(name = "raw_category_key")
private String rawCategoryKey;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "part_category_id", nullable = false)
private PartCategory partCategory;
@Column(name = "platform_locked", nullable = false)
private Boolean platformLocked = false;
@Column(name = "slug", nullable = false, length = Integer.MAX_VALUE)
private String slug;
@OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
private Set<ProductOffer> offers = new HashSet<>();
@Column(name = "caliber", length = Integer.MAX_VALUE)
private String caliber;
@Column(name = "barrel_length_mm")
private Integer barrelLengthMm;
@Column(name = "gas_system", length = Integer.MAX_VALUE)
private String gasSystem;
@Column(name = "handguard_length_mm")
private Integer handguardLengthMm;
@Column(name = "image_url", length = Integer.MAX_VALUE)
private String imageUrl;
@Column(name = "spec_sheet_url", length = Integer.MAX_VALUE)
private String specSheetUrl;
@Column(name = "msrp", precision = 10, scale = 2)
private BigDecimal msrp;
@Column(name = "current_lowest_price", precision = 10, scale = 2)
private BigDecimal currentLowestPrice;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "current_lowest_merchant_id")
private Merchant currentLowestMerchant;
@ColumnDefault("true")
@Column(name = "is_active", nullable = false)
private Boolean isActive = false;
public Boolean getIsActive() {
return isActive;
public Set<ProductOffer> getOffers() {
return offers;
}
public void setIsActive(Boolean isActive) {
this.isActive = isActive;
public void setOffers(Set<ProductOffer> offers) {
this.offers = offers;
}
public Merchant getCurrentLowestMerchant() {
return currentLowestMerchant;
// --- lifecycle hooks ---
@PrePersist
public void prePersist() {
if (uuid == null) {
uuid = UUID.randomUUID();
}
Instant now = Instant.now();
if (createdAt == null) {
createdAt = now;
}
if (updatedAt == null) {
updatedAt = now;
}
}
public void setCurrentLowestMerchant(Merchant currentLowestMerchant) {
this.currentLowestMerchant = currentLowestMerchant;
@PreUpdate
public void preUpdate() {
updatedAt = Instant.now();
}
public BigDecimal getCurrentLowestPrice() {
return currentLowestPrice;
public String getRawCategoryKey() {
return rawCategoryKey;
}
public void setCurrentLowestPrice(BigDecimal currentLowestPrice) {
this.currentLowestPrice = currentLowestPrice;
public void setRawCategoryKey(String rawCategoryKey) {
this.rawCategoryKey = rawCategoryKey;
}
public BigDecimal getMsrp() {
return msrp;
// --- getters & setters ---
public Integer getId() {
return id;
}
public void setMsrp(BigDecimal msrp) {
this.msrp = msrp;
public void setId(Integer id) {
this.id = id;
}
public String getSpecSheetUrl() {
return specSheetUrl;
public UUID getUuid() {
return uuid;
}
public void setSpecSheetUrl(String specSheetUrl) {
this.specSheetUrl = specSheetUrl;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public Integer getHandguardLengthMm() {
return handguardLengthMm;
}
public void setHandguardLengthMm(Integer handguardLengthMm) {
this.handguardLengthMm = handguardLengthMm;
}
public String getGasSystem() {
return gasSystem;
}
public void setGasSystem(String gasSystem) {
this.gasSystem = gasSystem;
}
public Integer getBarrelLengthMm() {
return barrelLengthMm;
}
public void setBarrelLengthMm(Integer barrelLengthMm) {
this.barrelLengthMm = barrelLengthMm;
}
public String getCaliber() {
return caliber;
}
public void setCaliber(String caliber) {
this.caliber = caliber;
}
public String getSlug() {
return slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
public PartCategory getPartCategory() {
return partCategory;
}
public void setPartCategory(PartCategory partCategory) {
this.partCategory = partCategory;
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
public Brand getBrand() {
@@ -190,14 +159,6 @@ public class Product {
this.brand = brand;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
@@ -206,6 +167,54 @@ public class Product {
this.name = name;
}
public String getSlug() {
return slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
public String getMpn() {
return mpn;
}
public void setMpn(String mpn) {
this.mpn = mpn;
}
public String getUpc() {
return upc;
}
public void setUpc(String upc) {
this.upc = upc;
}
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getPartRole() {
return partRole;
}
public void setPartRole(String partRole) {
this.partRole = partRole;
}
public String getShortDescription() {
return shortDescription;
}
public void setShortDescription(String shortDescription) {
this.shortDescription = shortDescription;
}
public String getDescription() {
return description;
}
@@ -214,44 +223,12 @@ public class Product {
this.description = description;
}
public BigDecimal getPrice() {
return price;
public String getMainImageUrl() {
return mainImageUrl;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public Integer getResellerId() {
return resellerId;
}
public void setResellerId(Integer resellerId) {
this.resellerId = resellerId;
}
public Integer getCategoryId() {
return categoryId;
}
public void setCategoryId(Integer categoryId) {
this.categoryId = categoryId;
}
public Integer getStockQty() {
return stockQty;
}
public void setStockQty(Integer stockQty) {
this.stockQty = stockQty;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
public void setMainImageUrl(String mainImageUrl) {
this.mainImageUrl = mainImageUrl;
}
public Instant getCreatedAt() {
@@ -262,6 +239,14 @@ public class Product {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
public Instant getDeletedAt() {
return deletedAt;
}
@@ -270,4 +255,56 @@ public class Product {
this.deletedAt = deletedAt;
}
public Boolean getPlatformLocked() {
return platformLocked;
}
public void setPlatformLocked(Boolean platformLocked) {
this.platformLocked = platformLocked;
}
public ProductConfiguration getConfiguration() {
return configuration;
}
public void setConfiguration(ProductConfiguration configuration) {
this.configuration = configuration;
}
// Convenience: best offer price for Gunbuilder
public BigDecimal getBestOfferPrice() {
if (offers == null || offers.isEmpty()) {
return BigDecimal.ZERO;
}
return offers.stream()
// pick sale_price if present, otherwise retail_price
.map(offer -> {
if (offer.getSalePrice() != null) {
return offer.getSalePrice();
}
return offer.getRetailPrice();
})
.filter(Objects::nonNull)
.min(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO);
}
// Convenience: URL for the best-priced offer
public String getBestOfferBuyUrl() {
if (offers == null || offers.isEmpty()) {
return null;
}
return offers.stream()
.sorted(Comparator.comparing(offer -> {
if (offer.getSalePrice() != null) {
return offer.getSalePrice();
}
return offer.getRetailPrice();
}, Comparator.nullsLast(BigDecimal::compareTo)))
.map(ProductOffer::getAffiliateUrl)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
}

View File

@@ -1,93 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import java.time.Instant;
@Entity
@Table(name = "product_categories")
public class ProductCategory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "name", nullable = false, length = 100)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_category_id")
private ProductCategory parentCategory;
@Column(name = "type", length = 50)
private String type;
@Column(name = "sort_order")
private Integer sortOrder;
@ColumnDefault("now()")
@Column(name = "created_at")
private Instant createdAt;
@ColumnDefault("now()")
@Column(name = "updated_at")
private Instant updatedAt;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ProductCategory getParentCategory() {
return parentCategory;
}
public void setParentCategory(ProductCategory parentCategory) {
this.parentCategory = parentCategory;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Integer getSortOrder() {
return sortOrder;
}
public void setSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,10 @@
package group.goforward.ballistic.model;
public enum ProductConfiguration {
STRIPPED, // bare receiver / component
ASSEMBLED, // built up but not fully complete
BARRELED, // upper + barrel + gas system, no BCG/CH
COMPLETE, // full assembly ready to run
KIT, // collection of parts (LPK, trigger kits, etc.)
OTHER // fallback / unknown
}

View File

@@ -1,107 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "product_feeds")
public class ProductFeed {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "reseller_id", nullable = false)
private Integer resellerId;
@Column(name = "feed_url", nullable = false)
private String feedUrl;
@Column(name = "last_update")
private Instant lastUpdate;
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "deleted_at")
private Instant deletedAt;
@ColumnDefault("gen_random_uuid()")
@Column(name = "uuid")
private UUID uuid;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getResellerId() {
return resellerId;
}
public void setResellerId(Integer resellerId) {
this.resellerId = resellerId;
}
public String getFeedUrl() {
return feedUrl;
}
public void setFeedUrl(String feedUrl) {
this.feedUrl = feedUrl;
}
public Instant getLastUpdate() {
return lastUpdate;
}
public void setLastUpdate(Instant lastUpdate) {
this.lastUpdate = lastUpdate;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(Instant deletedAt) {
this.deletedAt = deletedAt;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
}

View File

@@ -7,15 +7,15 @@ import org.hibernate.annotations.OnDeleteAction;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@Table(name = "product_offers")
public class ProductOffer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private UUID id;
private Integer id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@OnDelete(action = OnDeleteAction.CASCADE)
@@ -26,16 +26,16 @@ public class ProductOffer {
@JoinColumn(name = "merchant_id", nullable = false)
private Merchant merchant;
@Column(name = "avantlink_product_id", nullable = false, length = Integer.MAX_VALUE)
@Column(name = "avantlink_product_id", nullable = false)
private String avantlinkProductId;
@Column(name = "sku", length = Integer.MAX_VALUE)
@Column(name = "sku")
private String sku;
@Column(name = "upc", length = Integer.MAX_VALUE)
@Column(name = "upc")
private String upc;
@Column(name = "buy_url", nullable = false, length = Integer.MAX_VALUE)
@Column(name = "buy_url", nullable = false)
private String buyUrl;
@Column(name = "price", nullable = false, precision = 10, scale = 2)
@@ -45,7 +45,7 @@ public class ProductOffer {
private BigDecimal originalPrice;
@ColumnDefault("'USD'")
@Column(name = "currency", nullable = false, length = Integer.MAX_VALUE)
@Column(name = "currency", nullable = false)
private String currency;
@ColumnDefault("true")
@@ -60,11 +60,15 @@ public class ProductOffer {
@Column(name = "first_seen_at", nullable = false)
private OffsetDateTime firstSeenAt;
public UUID getId() {
// -----------------------------------------------------
// Getters & setters
// -----------------------------------------------------
public Integer getId() {
return id;
}
public void setId(UUID id) {
public void setId(Integer id) {
this.id = id;
}
@@ -164,4 +168,26 @@ public class ProductOffer {
this.firstSeenAt = firstSeenAt;
}
// -----------------------------------------------------
// Helper Methods (used by Product entity)
// -----------------------------------------------------
public BigDecimal getSalePrice() {
return price;
}
public BigDecimal getRetailPrice() {
return originalPrice != null ? originalPrice : price;
}
public String getAffiliateUrl() {
return buyUrl;
}
public BigDecimal getEffectivePrice() {
if (price != null && originalPrice != null && price.compareTo(originalPrice) < 0) {
return price;
}
return price != null ? price : originalPrice;
}
}

View File

@@ -1,357 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
@Entity
@Table(name = "psa")
public class Psa {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "\"SKU\"", length = 50)
private String sku;
@Column(name = "\"MANUFACTURER_ID\"", length = 50)
private String manufacturerId;
@Column(name = "\"BRAND_NAME\"", length = 50)
private String brandName;
@Column(name = "\"PRODUCT_NAME\"")
private String productName;
@Column(name = "\"LONG_DESCRIPTION\"", length = Integer.MAX_VALUE)
private String longDescription;
@Column(name = "\"SHORT_DESCRIPTION\"", length = 50)
private String shortDescription;
@Column(name = "\"DEPARTMENT\"", length = 50)
private String department;
@Column(name = "\"CATEGORY\"", length = 50)
private String category;
@Column(name = "\"SUBCATEGORY\"", length = 50)
private String subcategory;
@Column(name = "\"THUMB_URL\"", length = 50)
private String thumbUrl;
@Column(name = "\"IMAGE_URL\"", length = 50)
private String imageUrl;
@Column(name = "\"BUY_LINK\"", length = 128)
private String buyLink;
@Column(name = "\"KEYWORDS\"", length = 50)
private String keywords;
@Column(name = "\"REVIEWS\"", length = 50)
private String reviews;
@Column(name = "\"RETAIL_PRICE\"")
private Float retailPrice;
@Column(name = "\"SALE_PRICE\"")
private Float salePrice;
@Column(name = "\"BRAND_PAGE_LINK\"", length = 50)
private String brandPageLink;
/* @Column(name = "\"BRAND_LOGO_IMAGE\"", length = 50)
private String brandLogoImage;*/
@Column(name = "\"PRODUCT_PAGE_VIEW_TRACKING\"", length = 256)
private String productPageViewTracking;
@Column(name = "\"PARENT_GROUP_ID\"", length = 50)
private String parentGroupId;
@Column(name = "\"FINELINE\"", length = 50)
private String fineline;
@Column(name = "\"SUPERFINELINE\"", length = 200)
private String superfineline;
@Column(name = "\"MODELNUMBER\"", length = 50)
private String modelnumber;
@Column(name = "\"CALIBER\"", length = 200)
private String caliber;
@Column(name = "\"UPC\"", length = 100)
private String upc;
@Column(name = "\"MEDIUM_IMAGE_URL\"", length = 50)
private String mediumImageUrl;
@Column(name = "\"PRODUCT_CONTENT_WIDGET\"", length = 256)
private String productContentWidget;
@Column(name = "\"GOOGLE_CATEGORIZATION\"", length = 50)
private String googleCategorization;
@Column(name = "\"ITEM_BASED_COMMISSION\"", length = 50)
private String itemBasedCommission;
@ColumnDefault("gen_random_uuid()")
@Column(name = "uuid")
private String uuid;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getSku() {
return sku;
}
public void setSku(String sku) {
this.sku = sku;
}
public String getManufacturerId() {
return manufacturerId;
}
public void setManufacturerId(String manufacturerId) {
this.manufacturerId = manufacturerId;
}
public String getBrandName() {
return brandName;
}
public void setBrandName(String brandName) {
this.brandName = brandName;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getLongDescription() {
return longDescription;
}
public void setLongDescription(String longDescription) {
this.longDescription = longDescription;
}
public String getShortDescription() {
return shortDescription;
}
public void setShortDescription(String shortDescription) {
this.shortDescription = shortDescription;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getSubcategory() {
return subcategory;
}
public void setSubcategory(String subcategory) {
this.subcategory = subcategory;
}
public String getThumbUrl() {
return thumbUrl;
}
public void setThumbUrl(String thumbUrl) {
this.thumbUrl = thumbUrl;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getBuyLink() {
return buyLink;
}
public void setBuyLink(String buyLink) {
this.buyLink = buyLink;
}
public String getKeywords() {
return keywords;
}
public void setKeywords(String keywords) {
this.keywords = keywords;
}
public String getReviews() {
return reviews;
}
public void setReviews(String reviews) {
this.reviews = reviews;
}
public Float getRetailPrice() {
return retailPrice;
}
public void setRetailPrice(Float retailPrice) {
this.retailPrice = retailPrice;
}
public Float getSalePrice() {
return salePrice;
}
public void setSalePrice(Float salePrice) {
this.salePrice = salePrice;
}
public String getBrandPageLink() {
return brandPageLink;
}
public void setBrandPageLink(String brandPageLink) {
this.brandPageLink = brandPageLink;
}
/*
public String getBrandLogoImage() {
return brandLogoImage;
}
public void setBrandLogoImage(String brandLogoImage) {
this.brandLogoImage = brandLogoImage;
}
*/
public String getProductPageViewTracking() {
return productPageViewTracking;
}
public void setProductPageViewTracking(String productPageViewTracking) {
this.productPageViewTracking = productPageViewTracking;
}
public String getParentGroupId() {
return parentGroupId;
}
public void setParentGroupId(String parentGroupId) {
this.parentGroupId = parentGroupId;
}
public String getFineline() {
return fineline;
}
public void setFineline(String fineline) {
this.fineline = fineline;
}
public String getSuperfineline() {
return superfineline;
}
public void setSuperfineline(String superfineline) {
this.superfineline = superfineline;
}
public String getModelnumber() {
return modelnumber;
}
public void setModelnumber(String modelnumber) {
this.modelnumber = modelnumber;
}
public String getCaliber() {
return caliber;
}
public void setCaliber(String caliber) {
this.caliber = caliber;
}
public String getUpc() {
return upc;
}
public void setUpc(String upc) {
this.upc = upc;
}
public String getMediumImageUrl() {
return mediumImageUrl;
}
public void setMediumImageUrl(String mediumImageUrl) {
this.mediumImageUrl = mediumImageUrl;
}
public String getProductContentWidget() {
return productContentWidget;
}
public void setProductContentWidget(String productContentWidget) {
this.productContentWidget = productContentWidget;
}
public String getGoogleCategorization() {
return googleCategorization;
}
public void setGoogleCategorization(String googleCategorization) {
this.googleCategorization = googleCategorization;
}
public String getItemBasedCommission() {
return itemBasedCommission;
}
public void setItemBasedCommission(String itemBasedCommission) {
this.itemBasedCommission = itemBasedCommission;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
}

View File

@@ -1,23 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "psa_old")
public class PsaOld {
@Id
@Column(name = "\"SKU\"", length = 50)
private String sku;
public String getSku() {
return sku;
}
public void setSku(String sku) {
this.sku = sku;
}
}

View File

@@ -1,73 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault;
import java.time.Instant;
import java.time.OffsetDateTime;
@Entity
@Table(name = "sessions")
public class Session {
@Id
@Column(name = "id", nullable = false)
private String id;
@Column(name = "user_id", nullable = false, length = 21)
private String userId;
@Column(name = "expires_at", nullable = false)
private OffsetDateTime expiresAt;
@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "created_at")
private Instant createdAt;
@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "updated_at")
private Instant updatedAt;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public OffsetDateTime getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(OffsetDateTime expiresAt) {
this.expiresAt = expiresAt;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -1,236 +1,91 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import org.hibernate.annotations.ColumnDefault;
import java.time.Instant;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@Table(name = "users")
public class User {
@Id
@Column(name = "id", nullable = false, length = 21)
private String id;
@Column(name = "name", length = Integer.MAX_VALUE)
private String name;
@Column(name = "username", length = 50)
private String username;
@Column(name = "email", nullable = false)
private String email;
@Column(name = "first_name", length = 50)
private String firstName;
@Column(name = "last_name", length = 50)
private String lastName;
@Column(name = "full_name", length = 50)
private String fullName;
@Column(name = "profile_picture")
private String profilePicture;
@Column(name = "image", length = Integer.MAX_VALUE)
private String image;
@Column(name = "date_of_birth")
private LocalDate dateOfBirth;
@Column(name = "phone_number", length = 20)
private String phoneNumber;
@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "created_at")
private Instant createdAt;
@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "updated_at")
private Instant updatedAt;
@ColumnDefault("false")
@Column(name = "is_admin")
private Boolean isAdmin;
@Column(name = "last_login")
private Instant lastLogin;
@ColumnDefault("false")
@Column(name = "email_verified", nullable = false)
private Boolean emailVerified = false;
@ColumnDefault("'public'")
@Column(name = "build_privacy_setting", length = Integer.MAX_VALUE)
private String buildPrivacySetting;
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Integer id;
@NotNull
@ColumnDefault("gen_random_uuid()")
@Column(name = "uuid")
@Column(name = "uuid", nullable = false)
private UUID uuid;
@Column(name = "discord_id")
private String discordId;
@NotNull
@Column(name = "email", nullable = false, length = Integer.MAX_VALUE)
private String email;
@Column(name = "hashed_password")
private String hashedPassword;
@NotNull
@Column(name = "password_hash", nullable = false, length = Integer.MAX_VALUE)
private String passwordHash;
@Column(name = "avatar")
private String avatar;
@Column(name = "display_name", length = Integer.MAX_VALUE)
private String displayName;
@Column(name = "stripe_subscription_id", length = 191)
private String stripeSubscriptionId;
@NotNull
@ColumnDefault("'USER'")
@Column(name = "role", nullable = false, length = Integer.MAX_VALUE)
private String role;
@Column(name = "stripe_price_id", length = 191)
private String stripePriceId;
@NotNull
@ColumnDefault("true")
@Column(name = "is_active", nullable = false)
private boolean isActive = true;
@Column(name = "stripe_customer_id", length = 191)
private String stripeCustomerId;
@NotNull
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@Column(name = "stripe_current_period_end")
private Instant stripeCurrentPeriodEnd;
@NotNull
@ColumnDefault("now()")
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
public String getId() {
@Column(name = "deleted_at")
private OffsetDateTime deletedAt;
// NEW FIELDS
@Column(name = "email_verified_at")
private OffsetDateTime emailVerifiedAt;
@Column(name = "verification_token", length = Integer.MAX_VALUE)
private String verificationToken;
@Column(name = "reset_password_token", length = Integer.MAX_VALUE)
private String resetPasswordToken;
@Column(name = "reset_password_expires_at")
private OffsetDateTime resetPasswordExpiresAt;
@Column(name = "last_login_at")
private OffsetDateTime lastLoginAt;
@ColumnDefault("0")
@Column(name = "login_count", nullable = false)
private Integer loginCount = 0;
// --- Getters / setters ---
public Integer getId() {
return id;
}
public void setId(String id) {
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getProfilePicture() {
return profilePicture;
}
public void setProfilePicture(String profilePicture) {
this.profilePicture = profilePicture;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public LocalDate getDateOfBirth() {
return dateOfBirth;
}
public void setDateOfBirth(LocalDate dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
public Boolean getIsAdmin() {
return isAdmin;
}
public void setIsAdmin(Boolean isAdmin) {
this.isAdmin = isAdmin;
}
public Instant getLastLogin() {
return lastLogin;
}
public void setLastLogin(Instant lastLogin) {
this.lastLogin = lastLogin;
}
public Boolean getEmailVerified() {
return emailVerified;
}
public void setEmailVerified(Boolean emailVerified) {
this.emailVerified = emailVerified;
}
public String getBuildPrivacySetting() {
return buildPrivacySetting;
}
public void setBuildPrivacySetting(String buildPrivacySetting) {
this.buildPrivacySetting = buildPrivacySetting;
}
public UUID getUuid() {
return uuid;
}
@@ -239,60 +94,129 @@ public class User {
this.uuid = uuid;
}
public String getDiscordId() {
return discordId;
public String getEmail() {
return email;
}
public void setDiscordId(String discordId) {
this.discordId = discordId;
public void setEmail(String email) {
this.email = email;
}
public String getHashedPassword() {
return hashedPassword;
public String getPasswordHash() {
return passwordHash;
}
public void setHashedPassword(String hashedPassword) {
this.hashedPassword = hashedPassword;
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
public String getAvatar() {
return avatar;
public String getDisplayName() {
return displayName;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getStripeSubscriptionId() {
return stripeSubscriptionId;
public String getRole() {
return role;
}
public void setStripeSubscriptionId(String stripeSubscriptionId) {
this.stripeSubscriptionId = stripeSubscriptionId;
public void setRole(String role) {
this.role = role;
}
public String getStripePriceId() {
return stripePriceId;
public boolean getIsActive() {
return isActive;
}
public void setStripePriceId(String stripePriceId) {
this.stripePriceId = stripePriceId;
public void setIsActive(boolean active) {
isActive = active;
}
public String getStripeCustomerId() {
return stripeCustomerId;
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setStripeCustomerId(String stripeCustomerId) {
this.stripeCustomerId = stripeCustomerId;
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public Instant getStripeCurrentPeriodEnd() {
return stripeCurrentPeriodEnd;
public OffsetDateTime getUpdatedAt() {
return updatedAt;
}
public void setStripeCurrentPeriodEnd(Instant stripeCurrentPeriodEnd) {
this.stripeCurrentPeriodEnd = stripeCurrentPeriodEnd;
public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public OffsetDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(OffsetDateTime deletedAt) {
this.deletedAt = deletedAt;
}
public OffsetDateTime getEmailVerifiedAt() {
return emailVerifiedAt;
}
public void setEmailVerifiedAt(OffsetDateTime emailVerifiedAt) {
this.emailVerifiedAt = emailVerifiedAt;
}
public String getVerificationToken() {
return verificationToken;
}
public void setVerificationToken(String verificationToken) {
this.verificationToken = verificationToken;
}
public String getResetPasswordToken() {
return resetPasswordToken;
}
public void setResetPasswordToken(String resetPasswordToken) {
this.resetPasswordToken = resetPasswordToken;
}
public OffsetDateTime getResetPasswordExpiresAt() {
return resetPasswordExpiresAt;
}
public void setResetPasswordExpiresAt(OffsetDateTime resetPasswordExpiresAt) {
this.resetPasswordExpiresAt = resetPasswordExpiresAt;
}
public OffsetDateTime getLastLoginAt() {
return lastLoginAt;
}
public void setLastLoginAt(OffsetDateTime lastLoginAt) {
this.lastLoginAt = lastLoginAt;
}
public Integer getLoginCount() {
return loginCount;
}
public void setLoginCount(Integer loginCount) {
this.loginCount = loginCount;
}
// convenience helpers
@Transient
public boolean isEmailVerified() {
return emailVerifiedAt != null;
}
public void incrementLoginCount() {
if (loginCount == null) {
loginCount = 0;
}
loginCount++;
}
}

View File

@@ -1,47 +0,0 @@
package group.goforward.ballistic.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
@Entity
@Table(name = "\"verificationTokens\"")
public class VerificationToken {
@Id
@Column(name = "identifier", nullable = false, length = Integer.MAX_VALUE)
private String identifier;
@Column(name = "token", nullable = false, length = Integer.MAX_VALUE)
private String token;
@Column(name = "expires", nullable = false)
private Instant expires;
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Instant getExpires() {
return expires;
}
public void setExpires(Instant expires) {
this.expires = expires;
}
}

View File

@@ -0,0 +1 @@
package group.goforward.ballistic.model;

View File

@@ -1,5 +1,6 @@
package group.goforward.ballistic.model;
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.Account;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;

View File

@@ -0,0 +1,8 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.Brand;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface BrandRepository extends JpaRepository<Brand, Integer> {
Optional<Brand> findByNameIgnoreCase(String name);
}

View File

@@ -0,0 +1,12 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.BuildsComponent;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface BuildItemRepository extends JpaRepository<BuildsComponent, Integer> {
List<BuildsComponent> findByBuildId(Integer buildId);
Optional<BuildsComponent> findByUuid(UUID uuid);
}

View File

@@ -0,0 +1,10 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.Build;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface BuildRepository extends JpaRepository<Build, Integer> {
Optional<Build> findByUuid(UUID uuid);
}

View File

@@ -0,0 +1,22 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.CategoryMapping;
import group.goforward.ballistic.model.Merchant;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface CategoryMappingRepository extends JpaRepository<CategoryMapping, Integer> {
// All mappings for a merchant, ordered nicely
List<CategoryMapping> findByMerchantIdOrderByRawCategoryPathAsc(Integer merchantId);
// Merchants that actually have mappings (for the dropdown)
@Query("""
select distinct cm.merchant
from CategoryMapping cm
order by cm.merchant.name asc
""")
List<Merchant> findDistinctMerchantsWithMappings();
}

View File

@@ -0,0 +1,7 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.FeedImport;
import org.springframework.data.jpa.repository.JpaRepository;
public interface FeedImportRepository extends JpaRepository<FeedImport, Long> {
}

View File

@@ -0,0 +1,17 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.MerchantCategoryMapping;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MerchantCategoryMappingRepository
extends JpaRepository<MerchantCategoryMapping, Integer> {
Optional<MerchantCategoryMapping> findByMerchantIdAndRawCategoryIgnoreCase(
Integer merchantId,
String rawCategory
);
List<MerchantCategoryMapping> findByMerchantIdOrderByRawCategoryAsc(Integer merchantId);
}

View File

@@ -0,0 +1,11 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.Merchant;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface MerchantRepository extends JpaRepository<Merchant, Integer> {
Optional<Merchant> findByNameIgnoreCase(String name);
}

View File

@@ -0,0 +1,14 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.PartCategory;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface PartCategoryRepository extends JpaRepository<PartCategory, Integer> {
Optional<PartCategory> findBySlug(String slug);
List<PartCategory> findAllByOrderByGroupNameAscSortOrderAscNameAsc();
}

View File

@@ -0,0 +1,14 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.PartRoleCategoryMapping;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface PartRoleCategoryMappingRepository extends JpaRepository<PartRoleCategoryMapping, Integer> {
List<PartRoleCategoryMapping> findAllByPlatformOrderByPartRoleAsc(String platform);
Optional<PartRoleCategoryMapping> findByPlatformAndPartRole(String platform, String partRole);
}

View File

@@ -0,0 +1,12 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.PartRoleMapping;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PartRoleMappingRepository extends JpaRepository<PartRoleMapping, Integer> {
// List mappings for a platform, ordered nicely for the UI
List<PartRoleMapping> findByPlatformOrderByPartRoleAsc(String platform);
}

View File

@@ -0,0 +1,7 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.PriceHistory;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PriceHistoryRepository extends JpaRepository<PriceHistory, Long> {
}

View File

@@ -0,0 +1,22 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.ProductOffer;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public interface ProductOfferRepository extends JpaRepository<ProductOffer, Integer> {
List<ProductOffer> findByProductId(Integer productId);
// Used by the /api/products/gunbuilder endpoint
List<ProductOffer> findByProductIdIn(Collection<Integer> productIds);
// Unique offer lookup for importer upsert
Optional<ProductOffer> findByMerchantIdAndAvantlinkProductId(
Integer merchantId,
String avantlinkProductId
);
}

View File

@@ -0,0 +1,65 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.Brand;
import group.goforward.ballistic.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface ProductRepository extends JpaRepository<Product, Integer> {
// -------------------------------------------------
// Used by MerchantFeedImportServiceImpl
// -------------------------------------------------
List<Product> findAllByBrandAndMpn(Brand brand, String mpn);
List<Product> findAllByBrandAndUpc(Brand brand, String upc);
boolean existsBySlug(String slug);
// -------------------------------------------------
// Used by ProductController for platform views
// -------------------------------------------------
@Query("""
SELECT p
FROM Product p
JOIN FETCH p.brand b
WHERE p.platform = :platform
AND p.deletedAt IS NULL
""")
List<Product> findByPlatformWithBrand(@Param("platform") String platform);
@Query(name="Products.findByPlatformWithBrand")
List<Product> findByPlatformWithBrandNQ(@Param("platform") String platform);
@Query("""
SELECT p
FROM Product p
JOIN FETCH p.brand b
WHERE p.platform = :platform
AND p.partRole IN :roles
AND p.deletedAt IS NULL
""")
List<Product> findByPlatformAndPartRoleInWithBrand(
@Param("platform") String platform,
@Param("roles") List<String> roles
);
// -------------------------------------------------
// Used by Gunbuilder service (if you wired this)
// -------------------------------------------------
@Query("""
SELECT DISTINCT p
FROM Product p
LEFT JOIN FETCH p.brand b
LEFT JOIN FETCH p.offers o
WHERE p.platform = :platform
AND p.deletedAt IS NULL
""")
List<Product> findSomethingForGunbuilder(@Param("platform") String platform);
}

View File

@@ -1,7 +0,0 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.Psa;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface PsaRepository extends JpaRepository<Psa, UUID> {
}

View File

@@ -0,0 +1,16 @@
package group.goforward.ballistic.repos;
import group.goforward.ballistic.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface UserRepository extends JpaRepository<User, Integer> {
Optional<User> findByEmailIgnoreCaseAndDeletedAtIsNull(String email);
boolean existsByEmailIgnoreCaseAndDeletedAtIsNull(String email);
Optional<User> findByUuid(UUID uuid);
}

View File

@@ -0,0 +1,13 @@
/**
* Provides the classes necessary for the Spring Repository for the ballistic -Builder application.
* This package includes Repository for Spring-Boot application
*
*
* <p>The main entry point for managing the inventory is the
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
*
* @since 1.0
* @author Sean Strawsburg
* @version 1.1
*/
package group.goforward.ballistic.repos;

View File

@@ -0,0 +1,59 @@
package group.goforward.ballistic.security;
import group.goforward.ballistic.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class CustomUserDetails implements UserDetails {
private final User user;
private final List<GrantedAuthority> authorities;
public CustomUserDetails(User user) {
this.user = user;
this.authorities = List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
}
public User getUser() {
return user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return user.getPasswordHash();
}
@Override
public String getUsername() {
return user.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return user.getDeletedAt() == null;
}
@Override
public boolean isAccountNonLocked() {
return user.getIsActive();
}
@Override
public boolean isCredentialsNonExpired() {
return user.getDeletedAt() == null;
}
@Override
public boolean isEnabled() {
return user.getIsActive() && user.getDeletedAt() == null;
}
}

View File

@@ -0,0 +1,25 @@
package group.goforward.ballistic.security;
import group.goforward.ballistic.model.User;
import group.goforward.ballistic.repos.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository users;
public CustomUserDetailsService(UserRepository users) {
this.users = users;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = users.findByEmailIgnoreCaseAndDeletedAtIsNull(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return new CustomUserDetails(user);
}
}

View File

@@ -0,0 +1,26 @@
package group.goforward.ballistic.security;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException, ServletException {
// Simple JSON 401 response
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"Unauthorized\"}");
}
}

View File

@@ -0,0 +1,80 @@
package group.goforward.ballistic.security;
import group.goforward.ballistic.model.User;
import group.goforward.ballistic.repos.UserRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserRepository userRepository;
public JwtAuthenticationFilter(JwtService jwtService, UserRepository userRepository) {
this.jwtService = jwtService;
this.userRepository = userRepository;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (!StringUtils.hasText(authHeader) || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
if (!jwtService.isTokenValid(token)) {
filterChain.doFilter(request, response);
return;
}
UUID userUuid = jwtService.extractUserUuid(token);
if (userUuid == null || SecurityContextHolder.getContext().getAuthentication() != null) {
filterChain.doFilter(request, response);
return;
}
User user = userRepository.findByUuid(userUuid)
.orElse(null);
if (user == null || !user.getIsActive()) {
filterChain.doFilter(request, response);
return;
}
CustomUserDetails userDetails = new CustomUserDetails(user);
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,71 @@
package group.goforward.ballistic.security;
import group.goforward.ballistic.model.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
@Service
public class JwtService {
private final Key key;
private final long accessTokenMinutes;
public JwtService(
@Value("${security.jwt.secret}") String secret,
@Value("${security.jwt.access-token-minutes:60}") long accessTokenMinutes
) {
this.key = Keys.hmacShaKeyFor(secret.getBytes());
this.accessTokenMinutes = accessTokenMinutes;
}
public String generateToken(User user) {
Instant now = Instant.now();
Instant expiry = now.plus(accessTokenMinutes, ChronoUnit.MINUTES);
return Jwts.builder()
.setSubject(user.getUuid().toString())
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(expiry))
.addClaims(Map.of(
"email", user.getEmail(),
"role", user.getRole(),
"displayName", user.getDisplayName()
))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public UUID extractUserUuid(String token) {
Claims claims = parseClaims(token);
return UUID.fromString(claims.getSubject());
}
public boolean isTokenValid(String token) {
try {
parseClaims(token);
return true;
} catch (JwtException | IllegalArgumentException ex) {
return false;
}
}
private Claims parseClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}

View File

@@ -1,36 +0,0 @@
package group.goforward.ballistic.service;
import group.goforward.ballistic.model.Psa;
import group.goforward.ballistic.repos.PsaRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Service
public class PsaService {
private final PsaRepository psaRepository;
@Autowired
public PsaService(PsaRepository psaRepository) {
this.psaRepository = psaRepository;
}
public List<Psa> findAll() {
return psaRepository.findAll();
}
public Optional<Psa> findById(UUID id) {
return psaRepository.findById(id);
}
public Psa save(Psa psa) {
return psaRepository.save(psa);
}
public void deleteById(UUID id) {
psaRepository.deleteById(id);
}
}

View File

@@ -0,0 +1,16 @@
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.Brand;
import java.util.List;
import java.util.Optional;
public interface BrandService {
List<Brand> findAll();
Optional<Brand> findById(Integer id);
Brand save(Brand item);
void deleteById(Integer id);
}

View File

@@ -0,0 +1,57 @@
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.PartCategory;
import group.goforward.ballistic.model.Product;
import group.goforward.ballistic.repos.ProductRepository;
import group.goforward.ballistic.web.dto.GunbuilderProductDto;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class GunbuilderProductService {
private final ProductRepository productRepository;
private final PartCategoryResolverService partCategoryResolverService;
public GunbuilderProductService(
ProductRepository productRepository,
PartCategoryResolverService partCategoryResolverService
) {
this.productRepository = productRepository;
this.partCategoryResolverService = partCategoryResolverService;
}
public List<GunbuilderProductDto> listGunbuilderProducts(String platform) {
List<Product> products = productRepository.findSomethingForGunbuilder(platform);
return products.stream()
.map(p -> {
var maybeCategory = partCategoryResolverService
.resolveForPlatformAndPartRole(platform, p.getPartRole());
if (maybeCategory.isEmpty()) {
// you can also log here
return null;
}
PartCategory cat = maybeCategory.get();
return new GunbuilderProductDto(
p.getId(),
p.getName(),
p.getBrand().getName(),
platform,
p.getPartRole(),
p.getBestOfferPrice(),
p.getMainImageUrl(),
p.getBestOfferBuyUrl(),
cat.getSlug(),
cat.getGroupName()
);
})
.filter(dto -> dto != null)
.toList();
}
}

View File

@@ -0,0 +1,96 @@
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.Merchant;
import group.goforward.ballistic.model.MerchantCategoryMapping;
import group.goforward.ballistic.model.ProductConfiguration;
import group.goforward.ballistic.repos.MerchantCategoryMappingRepository;
import jakarta.transaction.Transactional;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
@Service
public class MerchantCategoryMappingService {
private final MerchantCategoryMappingRepository mappingRepository;
public MerchantCategoryMappingService(MerchantCategoryMappingRepository mappingRepository) {
this.mappingRepository = mappingRepository;
}
public List<MerchantCategoryMapping> findByMerchant(Integer merchantId) {
return mappingRepository.findByMerchantIdOrderByRawCategoryAsc(merchantId);
}
/**
* Resolve (or create) a mapping row for this merchant + raw category.
* - If it exists, returns it (with whatever mappedPartRole / mappedConfiguration are set).
* - If it doesn't exist, creates a placeholder row with null mappings and returns it.
*
* The importer can then:
* - skip rows where mappedPartRole is still null
* - use mappedConfiguration if present
*/
@Transactional
public MerchantCategoryMapping resolveMapping(Merchant merchant, String rawCategory) {
if (rawCategory == null || rawCategory.isBlank()) {
return null;
}
String trimmed = rawCategory.trim();
return mappingRepository
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
.orElseGet(() -> {
MerchantCategoryMapping mapping = new MerchantCategoryMapping();
mapping.setMerchant(merchant);
mapping.setRawCategory(trimmed);
mapping.setMappedPartRole(null);
mapping.setMappedConfiguration(null);
return mappingRepository.save(mapping);
});
}
/**
* Upsert mapping (admin UI).
*/
@Transactional
public MerchantCategoryMapping upsertMapping(
Merchant merchant,
String rawCategory,
String mappedPartRole,
ProductConfiguration mappedConfiguration
) {
String trimmed = rawCategory.trim();
MerchantCategoryMapping mapping = mappingRepository
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
.orElseGet(() -> {
MerchantCategoryMapping m = new MerchantCategoryMapping();
m.setMerchant(merchant);
m.setRawCategory(trimmed);
return m;
});
mapping.setMappedPartRole(
(mappedPartRole == null || mappedPartRole.isBlank()) ? null : mappedPartRole.trim()
);
mapping.setMappedConfiguration(mappedConfiguration);
return mappingRepository.save(mapping);
}
/**
* Backwards-compatible overload for existing callers (e.g. controller)
* that dont care about productConfiguration yet.
*/
@Transactional
public MerchantCategoryMapping upsertMapping(
Merchant merchant,
String rawCategory,
String mappedPartRole
) {
// Delegate to the new method with `null` configuration
return upsertMapping(merchant, rawCategory, mappedPartRole, null);
}
}

Some files were not shown because too many files have changed in this diff Show More