Removed unused components, services, and database schema related to nomenclature and custom tariff handling, seperated mass edit and single edit in frontend:

- **Frontend**: Deleted `SelectNode.vue` component and related styles.
- **Backend**: Removed `CustomController`, `ChangeMaterialService`, `ChangeSupplierService`, `NomenclatureService`, `NomenclatureRepository`, and unused DTOs.
- **Database**: Dropped `nomenclature` table and eliminated associated migration script.
- **Other**: Cleaned up imports and references to the removed implementations throughout the codebase.
This commit is contained in:
Jan 2025-11-14 20:34:05 +01:00
parent b99e7b3b4f
commit 305033e8f0
38 changed files with 981 additions and 18010 deletions

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="container" :class="massEditClasses"> <div class="container">
<div class="search-bar-container"> <div class="search-bar-container">
<autosuggest-searchbar class="search-bar" <autosuggest-searchbar class="search-bar"
placeholder="Add new Destination ..." placeholder="Add new Destination ..."
@ -23,8 +23,6 @@
<div>Action</div> <div>Action</div>
</div> </div>
<!-- TODO: straight this up -->
<div v-if="showDestinationsList"> <div v-if="showDestinationsList">
<destination-item v-for="destination in destinations" :key="destination.id" <destination-item v-for="destination in destinations" :key="destination.id"
:id="destination.id" :destination="destination" @delete="deleteDestination" :id="destination.id" :destination="destination" @delete="deleteDestination"
@ -36,8 +34,9 @@
</transition-group> </transition-group>
<!-- </div>--> <!-- </div>-->
<modal :state="editDestinationModalState" > <modal :state="editDestinationModalState">
<destination-edit @accept="deselectDestination(true)" @discard="deselectDestination(false)"></destination-edit> <destination-edit @accept="editDestinationFinish(true)"
@discard="editDestinationFinish(false)"></destination-edit>
</modal> </modal>
</div> </div>
</template> </template>
@ -53,11 +52,12 @@ import Checkbox from "@/components/UI/Checkbox.vue";
import CalculationListItem from "@/components/layout/calculation/CalculationListItem.vue"; import CalculationListItem from "@/components/layout/calculation/CalculationListItem.vue";
import DestinationItem from "@/components/layout/edit/destination/DestinationItem.vue"; import DestinationItem from "@/components/layout/edit/destination/DestinationItem.vue";
import {mapStores} from "pinia"; import {mapStores} from "pinia";
import {usePremiseEditStore} from "@/store/premiseEdit.js";
import {useNodeStore} from "@/store/node.js"; import {useNodeStore} from "@/store/node.js";
import Modal from "@/components/UI/Modal.vue"; import Modal from "@/components/UI/Modal.vue";
import DestinationEdit from "@/components/layout/edit/destination/DestinationEdit.vue"; import DestinationEdit from "@/components/layout/edit/destination/DestinationEdit.vue";
import {UrlSafeBase64} from "@/common.js"; import {usePremiseSingleEditStore} from "@/store/premiseSingleEdit.js";
import logger from "@/logger.js";
import {useDestinationSingleEditStore} from "@/store/destinationSingleEdit.js";
export default { export default {
name: "DestinationListView", name: "DestinationListView",
@ -72,12 +72,9 @@ export default {
} }
}, },
computed: { computed: {
massEditClasses() { ...mapStores(usePremiseSingleEditStore, useDestinationSingleEditStore, useNodeStore),
return this.premiseEditStore.isSingleSelect ? '' : 'container--mass-edit';
},
...mapStores(usePremiseEditStore, useNodeStore),
destinations() { destinations() {
return this.premiseEditStore.getDestinationsView; return this.premiseSingleEditStore.getDestinations;
}, },
showDestinationsList() { showDestinationsList() {
return this.destinations !== null && this.destinations.length > 0; return this.destinations !== null && this.destinations.length > 0;
@ -92,22 +89,35 @@ export default {
return node.country.iso_code; return node.country.iso_code;
}, },
async addDestination(node) { async addDestination(node) {
console.log(node) logger.log(node)
const [id] = await this.premiseEditStore.addDestination(node); const [id] = await this.premiseSingleEditStore.addDestination(node);
this.editDestination(id); this.editDestination(id);
}, },
deleteDestination(id) { async deleteDestination(id) {
this.premiseEditStore.deleteDestination(id); this.premiseSingleEditStore.deleteDestination(id);
}, },
editDestination(id) { editDestination(id) {
// TODO refactor.
if (id && this.premiseEditStore.getDestinationById(id) !== null) { logger.log(id);
this.premiseEditStore.selectDestination(id);
if (id) {
const destination = this.premiseSingleEditStore.getDestinationById(id);
logger.log(destination);
this.destinationSingleEditStore.setDestination(destination);
this.editDestinationModalState = true; this.editDestinationModalState = true;
} }
}, },
deselectDestination(save) { async editDestinationFinish(save) {
this.premiseEditStore.deselectDestinations(save);
if (save) {
const dest = this.premiseSingleEditStore.getDestinationById(this.destinationSingleEditStore.destination.id);
logger.log("id", this.destinationSingleEditStore.destination.id, "dest", dest);
this.destinationSingleEditStore.copyBack(dest);
await this.premiseSingleEditStore.updateDestination(this.destinationSingleEditStore.destination.id)
}
this.editDestinationModalState = false; this.editDestinationModalState = false;
} }
} }
@ -132,10 +142,6 @@ export default {
} }
.container--mass-edit {
width: min(80vw, 120rem);
}
.search-bar-container { .search-bar-container {
margin: 3rem 3rem 0 3rem; margin: 3rem 3rem 0 3rem;

View file

@ -18,13 +18,6 @@
<div class="supplier-map"> <div class="supplier-map">
<open-street-map-embed :coordinates="supplierCoordinates" :zoom="15" width="100%" height="300px" custom-filter="grayscale(0.8) sepia(0.5) hue-rotate(180deg) saturate(0.5) brightness(1.0)"></open-street-map-embed> <open-street-map-embed :coordinates="supplierCoordinates" :zoom="15" width="100%" height="300px" custom-filter="grayscale(0.8) sepia(0.5) hue-rotate(180deg) saturate(0.5) brightness(1.0)"></open-street-map-embed>
</div> </div>
<div class="footer">
<modal :state="selectSupplierModalState" @close="closeEditModal">
<select-node @update-supplier="modalDialogClose"></select-node>
</modal>
<!-- <icon-button icon="plus" @click="openModal"></icon-button>-->
<!-- <icon-button icon="pencil-simple" @click="openModal"></icon-button>-->
</div>
</div> </div>
</template> </template>
<script> <script>
@ -35,14 +28,12 @@ import InputField from "@/components/UI/InputField.vue";
import Flag from "@/components/UI/Flag.vue"; import Flag from "@/components/UI/Flag.vue";
import {PhUser} from "@phosphor-icons/vue"; import {PhUser} from "@phosphor-icons/vue";
import ModalDialog from "@/components/UI/ModalDialog.vue"; import ModalDialog from "@/components/UI/ModalDialog.vue";
import SelectNode from "@/components/layout/node/SelectNode.vue";
import Modal from "@/components/UI/Modal.vue"; import Modal from "@/components/UI/Modal.vue";
import OpenStreetMapEmbed from "@/components/UI/OpenStreetMapEmbed.vue"; import OpenStreetMapEmbed from "@/components/UI/OpenStreetMapEmbed.vue";
export default { export default {
name: "SupplierView", name: "SupplierView",
components: { OpenStreetMapEmbed, Modal, SelectNode, ModalDialog, PhUser, Flag, InputField, IconButton}, components: { OpenStreetMapEmbed, Modal, ModalDialog, PhUser, Flag, InputField, IconButton},
emits: ['updateSupplier'],
props: { props: {
supplierName: { supplierName: {
type: String, type: String,
@ -87,18 +78,6 @@ export default {
this.selectSupplierModalState = this.openSelectDirect; this.selectSupplierModalState = this.openSelectDirect;
}, },
methods: { methods: {
closeEditModal() {
this.selectSupplierModalState = false;
},
modalDialogClose(data) {
this.selectSupplierModalState = false;
if (data.action === 'accept') {
this.$emit('updateSupplier', data);
}
},
openModal() {
this.selectSupplierModalState = true;
},
convertToDMS(coordinate, type) { convertToDMS(coordinate, type) {
if (!coordinate) if (!coordinate)

View file

@ -44,6 +44,7 @@ import {mapStores} from "pinia";
import {usePremiseEditStore} from "@/store/premiseEdit.js"; import {usePremiseEditStore} from "@/store/premiseEdit.js";
import {set} from "@vueuse/core"; import {set} from "@vueuse/core";
import {parseNumberFromString} from "@/common.js"; import {parseNumberFromString} from "@/common.js";
import {useDestinationSingleEditStore} from "@/store/destinationSingleEdit.js";
export default { export default {
name: "DestinationEditHandlingCost", name: "DestinationEditHandlingCost",
@ -52,9 +53,9 @@ export default {
console.log("Destination:", this.destination) console.log("Destination:", this.destination)
}, },
computed: { computed: {
...mapStores(usePremiseEditStore), ...mapStores(useDestinationSingleEditStore),
destination() { destination() {
return this.premiseEditStore.getSelectedDestinationsData; return this.destinationSingleEditStore.destination;
}, },
repackaging: { repackaging: {
get() { get() {

View file

@ -78,9 +78,9 @@
import RadioOption from "@/components/UI/RadioOption.vue"; import RadioOption from "@/components/UI/RadioOption.vue";
import DestinationRoute from "@/components/layout/edit/destination/DestinationRoute.vue"; import DestinationRoute from "@/components/layout/edit/destination/DestinationRoute.vue";
import {mapStores} from "pinia"; import {mapStores} from "pinia";
import {usePremiseEditStore} from "@/store/premiseEdit.js";
import {parseNumberFromString} from "@/common.js"; import {parseNumberFromString} from "@/common.js";
import Tooltip from "@/components/UI/Tooltip.vue"; import Tooltip from "@/components/UI/Tooltip.vue";
import {useDestinationSingleEditStore} from "@/store/destinationSingleEdit.js";
export default { export default {
name: "DestinationEditRoutes", name: "DestinationEditRoutes",
@ -149,9 +149,9 @@ export default {
this.destination.is_d2d = value === 'd2d'; this.destination.is_d2d = value === 'd2d';
} }
}, },
...mapStores(usePremiseEditStore), ...mapStores(useDestinationSingleEditStore),
destination() { destination() {
return this.premiseEditStore.getSelectedDestinationsData; return this.destinationSingleEditStore.destination;
}, },
annualAmount: { annualAmount: {
get() { get() {

View file

@ -10,7 +10,7 @@
<div class="error-section"> <div class="error-section">
<div class="error-label">Type</div> <div class="error-label">Type</div>
<div class="error-value"> <div class="error-value">
<basic-badge :variant="badgeVariant">{{ error.type || 'UNKNOWN' }}</basic-badge> <basic-badge :icon="badgeIcon" :variant="badgeVariant">{{ error.type || 'UNKNOWN' }}</basic-badge>
</div> </div>
</div> </div>
@ -122,6 +122,18 @@ export default {
} }
}, },
computed: { computed: {
badgeIcon() {
if (this.error.type === "FRONTEND") {
return "desktop";
} else if (this.error.type === "BACKEND") {
return "hardDrives"
} else if (this.error.type === "BULK") {
return "stack"
} else if (this.error.type === "CALCULATION") {
return "calculator"
} else
return ""
},
badgeVariant() { badgeVariant() {
switch (this.error.type) { switch (this.error.type) {
case "FRONTEND": case "FRONTEND":
@ -131,7 +143,6 @@ export default {
default: default:
return "grey"; return "grey";
} }
}, },
formattedTimestamp() { formattedTimestamp() {
if (!this.error.timestamp) return 'N/A'; if (!this.error.timestamp) return 'N/A';

View file

@ -1,223 +0,0 @@
<template>
<div class="select-node-modal-container">
<h3 class="sub-header">Select supplier</h3>
<div class="select-node-container">
<div class="select-node-caption-column">Supplier</div>
<div class="select-node-input-column select-node-input-field-suppliername">
<div class="select-node-input-field-suppliername-searchbar">
<autosuggest-searchbar :fetch-suggestions="fetchSupplier"
placeholder="Find supplier..."
title-resolver="name"
subtitle-resolver="address"
:flag-resolver="resolveFlag"
variant="flags"
@selected="selected"
ref="searchbar"
:initial-value="initialValue"
:activate-watcher="true"
>
</autosuggest-searchbar>
</div>
</div>
<div class="select-node-caption-column" v-if="nodeSelected">Address</div>
<div class="select-node-input-column select-node-input-field-address" v-if="nodeSelected">
<div class="supplier-flag">
<flag :iso="isoCode"/>
</div>
<div class="supplier-address">{{ supplierAddress }}</div>
</div>
<div class="select-node-caption-column" v-if="nodeSelected">Coordinates</div>
<div class="select-node-input-column" v-if="nodeSelected">{{ coordinatesDMS }}</div>
<div class="select-node-checkbox" v-if="nodeSelected">
<checkbox :checked="updateMasterData" @checkbox-changed="checkboxChanged">update master data</checkbox>
</div>
<div class="select-node-footer">
<basic-button :show-icon="false" :disabled="!nodeSelected" @click="action('accept')">OK</basic-button>
<basic-button variant="secondary" :show-icon="false" @click="action('cancel')">Cancel</basic-button>
</div>
</div>
</div>
</template>
<script>
import BasicButton from "@/components/UI/BasicButton.vue";
import InputField from "@/components/UI/InputField.vue";
import Flag from "@/components/UI/Flag.vue";
import AutosuggestSearchbar from "@/components/UI/AutoSuggestSearchBar.vue";
import {mapStores} from "pinia";
import {useNodeStore} from "@/store/node.js";
import Checkbox from "@/components/UI/Checkbox.vue";
import logger from "@/logger.js";
export default {
name: "SelectNode",
components: {Checkbox, AutosuggestSearchbar, Flag, InputField, BasicButton},
emits: ['updateSupplier'],
props: {
openSelectDirect: {
type: Boolean,
default: false,
},
preSelectedNode: {
type: Object,
required: false
},
},
created() {
logger.info("SelectNode created with openSelectDirect: " + this.openSelectDirect, this.preSelectedNode);
if(this.openSelectDirect) {
this.node = this.preSelectedNode;
}
},
methods: {
action(action) {
this.$emit('updateSupplier', {action: action, nodeId: this.node?.id, updateMasterData: this.updateMasterData});
},
checkboxChanged(value) {
this.updateMasterData = value;
},
async fetchSupplier(query) {
logger.info("Fetching supplier for query: " + query);
await this.nodeStore.setSearch({searchTerm: query, nodeType: 'SOURCE', includeUserNode: true});
return this.nodeStore.nodes;
},
resolveFlag(node) {
return node.country.iso_code;
},
selected(node) {
logger.info("Selected node: ", node);
this.$refs.searchbar.clearSuggestions();
this.node = node;
},
convertToDMS(coordinate, type) {
if (!coordinate)
return '';
let direction;
if (type === 'lat') {
direction = coordinate >= 0 ? 'N' : 'S';
} else {
direction = coordinate >= 0 ? 'E' : 'W';
}
// Arbeite mit Absolutwert
const abs = Math.abs(coordinate);
// Grad (ganzzahliger Teil)
const degrees = Math.floor(abs);
// Minuten
const minutesFloat = (abs - degrees) * 60;
const minutes = minutesFloat.toFixed(4);
return `${degrees}° ${minutes}' ${direction}`;
}
},
data() {
return {
node: null,
updateMasterData: true,
}
},
computed: {
...mapStores(useNodeStore),
initialValue() {
if(this.node) {
return this.node.name;
} else {
return '';
}
},
supplierAddress() {
return this.node?.address ?? '';
},
nodeSelected() {
return this.node != null;
},
isoCode() {
return this.node?.country.iso_code ?? 'DE';
},
coordinatesDMS() {
return `${this.convertToDMS(this.node?.location?.latitude, 'lat')}, ${this.convertToDMS(this.node?.location?.longitude, 'lng')}`;
}
}
}
</script>
<style scoped>
.select-node-input-column {
font-size: 1.4rem;
color: #6b7280;
max-width: 30rem;
}
.select-node-input-field-suppliername {
display: flex;
gap: 0.8rem;
align-items: center;
}
.select-node-input-field-suppliername-searchbar {
flex: 1 1 auto;
min-width: 50rem;
}
.select-node-modal-container {
width: 60rem;
display: flex;
flex-direction: column;
gap: 1.6rem;
}
.select-node-container {
flex: 1 1 auto;
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: repeat(3, fit-content(0));
align-content: center;
gap: 1.6rem;
}
.select-node-caption-column {
font-size: 1.4rem;
font-weight: 500;
align-self: center;
justify-self: end;
color: #001D33
}
.select-node-footer {
grid-column: 1 / -1;
display: flex;
justify-content: flex-end;
gap: 1.6rem;
}
.select-node-checkbox {
grid-column: 1 / -1;
display: flex;
justify-content: flex-end;
gap: 1.6rem;
}
.select-node-input-field-address {
display: flex;
gap: 0.8rem;
align-items: center;
}
.supplier-address {
font-size: 1.4rem;
color: #6b7280;
max-width: 30rem;
}
</style>

View file

@ -156,7 +156,7 @@
<report-route :sections="premise.sections" :destination="premise.destination" <report-route :sections="premise.sections" :destination="premise.destination"
:route-section-scale="routeSectionScale[idx]"></report-route> :route-section-scale="routeSectionScale[idx]"></report-route>
<div class="report-sub-header">Premises</div> <div class="report-sub-header">General</div>
<div class="report-content-container--2-col"> <div class="report-content-container--2-col">

View file

@ -121,7 +121,6 @@ import PriceEdit from "@/components/layout/edit/PriceEdit.vue";
import MaterialEdit from "@/components/layout/edit/MaterialEdit.vue"; import MaterialEdit from "@/components/layout/edit/MaterialEdit.vue";
import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue"; import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
import DestinationListView from "@/components/layout/edit/DestinationListView.vue"; import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
import SelectNode from "@/components/layout/node/SelectNode.vue";
import Toast from "@/components/UI/Toast.vue"; import Toast from "@/components/UI/Toast.vue";
import logger from "@/logger.js"; import logger from "@/logger.js";
import {useCustomsStore} from "@/store/customs.js"; import {useCustomsStore} from "@/store/customs.js";
@ -131,7 +130,6 @@ const COMPONENT_TYPES = {
price: PriceEdit, price: PriceEdit,
material: MaterialEdit, material: MaterialEdit,
packaging: PackagingEdit, packaging: PackagingEdit,
supplier: SelectNode,
destinations: DestinationListView, destinations: DestinationListView,
} }
@ -414,11 +412,6 @@ export default {
mixable: premise.is_mixable ?? true, mixable: premise.is_mixable ?? true,
stackable: premise.is_stackable ?? true stackable: premise.is_stackable ?? true
} }
} else if (type === "supplier") {
this.componentsData.supplier.props = {
preSelectedNode: premise.supplier,
openSelectDirect: true
}
} }
} }
} }

View file

@ -3,12 +3,12 @@
<div class="header-container"> <div class="header-container">
<h2 class="page-header">Edit calculation</h2> <h2 class="page-header">Edit calculation</h2>
<div class="header-controls"> <div class="header-controls">
<basic-button @click="close" :show-icon="false" :disabled="premiseEditStore.selectedLoading" <basic-button @click="close" :show-icon="false" :disabled="premiseSingleEditStore.showLoadingSpinner"
variant="secondary"> {{ fromMassEdit ? 'Back' : 'Close' }} variant="secondary"> {{ fromMassEdit ? 'Back' : 'Close' }}
</basic-button> </basic-button>
<basic-button v-if="!fromMassEdit" <basic-button v-if="!fromMassEdit"
:show-icon="true" :show-icon="true"
:disabled="premiseEditStore.selectedLoading || !premiseEditStore.isSingleSelect" :disabled="premiseSingleEditStore.showLoadingSpinner || premiseSingleEditStore.isEmpty"
icon="Calculator" icon="Calculator"
variant="primary" variant="primary"
@click="startCalculation" @click="startCalculation"
@ -19,12 +19,12 @@
</div> </div>
<Toast ref="toast"/> <Toast ref="toast"/>
<div v-if="premiseEditStore.selectedLoading" class="edit-calculation-spinner-container"> <div v-if="premiseSingleEditStore.showLoadingSpinner" class="edit-calculation-spinner-container">
<box class="edit-calculation-spinner"> <box class="edit-calculation-spinner">
<spinner></spinner> <spinner></spinner>
</box> </box>
</div> </div>
<div v-else-if="!premiseEditStore.isSingleSelect" class="edit-calculation-spinner-container"> <div v-else-if="premiseSingleEditStore.isEmpty" class="edit-calculation-spinner-container">
<box class="edit-calculation-spinner">No calculation found.</box> <box class="edit-calculation-spinner">No calculation found.</box>
</div> </div>
<div v-else> <div v-else>
@ -36,7 +36,6 @@
:supplier-name="premise.supplier.name" :supplier-name="premise.supplier.name"
:supplier-coordinates="premise.supplier.location" :supplier-coordinates="premise.supplier.location"
:iso-code="premise.supplier.country.iso_code" :iso-code="premise.supplier.country.iso_code"
@update-supplier="updateSupplier"
></supplier-view> ></supplier-view>
</box> </box>
</div> </div>
@ -61,7 +60,6 @@
v-model:hs-code="premise.hs_code" v-model:hs-code="premise.hs_code"
v-model:tariff-rate="premise.tariff_rate" v-model:tariff-rate="premise.tariff_rate"
v-model:country-id="premise.supplier.country.id" v-model:country-id="premise.supplier.country.id"
@update-material="updateMaterial"
@save="save"></material-edit> @save="save"></material-edit>
</box> </box>
<box class="master-data-item"> <box class="master-data-item">
@ -108,7 +106,6 @@ import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
import PriceEdit from "@/components/layout/edit/PriceEdit.vue"; import PriceEdit from "@/components/layout/edit/PriceEdit.vue";
import DestinationListView from "@/components/layout/edit/DestinationListView.vue"; import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
import {mapStores} from "pinia"; import {mapStores} from "pinia";
import {usePremiseEditStore} from "@/store/premiseEdit.js";
import Spinner from "@/components/UI/Spinner.vue"; import Spinner from "@/components/UI/Spinner.vue";
import NotificationBar from "@/components/UI/NotificationBar.vue"; import NotificationBar from "@/components/UI/NotificationBar.vue";
import Modal from "@/components/UI/Modal.vue"; import Modal from "@/components/UI/Modal.vue";
@ -116,7 +113,7 @@ import TraceView from "@/components/layout/TraceView.vue";
import IconButton from "@/components/UI/IconButton.vue"; import IconButton from "@/components/UI/IconButton.vue";
import Toast from "@/components/UI/Toast.vue"; import Toast from "@/components/UI/Toast.vue";
import {UrlSafeBase64} from "@/common.js"; import {UrlSafeBase64} from "@/common.js";
import {useCustomsStore} from "@/store/customs.js"; import {usePremiseSingleEditStore} from "@/store/premiseSingleEdit.js";
export default { export default {
name: "SingleEdit", name: "SingleEdit",
@ -140,26 +137,25 @@ export default {
traceModal: false, traceModal: false,
bulkEditQuery: null, bulkEditQuery: null,
id: null, id: null,
processingMessage: "Please wait. Calculating ...",
showCalculationModal: false, showCalculationModal: false,
} }
}, },
computed: { computed: {
...mapStores(usePremiseEditStore, useCustomsStore), ...mapStores(usePremiseSingleEditStore),
premise() { premise() {
return this.premiseEditStore.singleSelectedPremise; return this.premiseSingleEditStore.premise;
}, },
fromMassEdit() { fromMassEdit() {
return this.bulkEditQuery !== null; return this.bulkEditQuery !== null;
}, },
showProcessingModal() { showProcessingModal() {
return this.premiseEditStore.showProcessingModal || this.showCalculationModal || this.customsStore.loadingTariff; return this.premiseSingleEditStore.showProcessingModal || this.showCalculationModal;
}, },
shownProcessingMessage() { shownProcessingMessage() {
if(this.customsStore.loadingTariff) if (this.premiseSingleEditStore.routing)
return "Looking up tariff rate ..." return "Please wait. Routing ..."
return this.processingMessage; return "Please wait. Calculating ...";
} }
}, },
@ -167,11 +163,11 @@ export default {
async startCalculation() { async startCalculation() {
this.showCalculationModal = true; this.showCalculationModal = true;
const error = await this.premiseEditStore.startCalculation(); const error = await this.premiseSingleEditStore.startCalculation();
if (error !== null) { if (error !== null) {
if(error.title === 'Internal Server Error') { if (error.title === 'Internal Server Error') {
this.$refs.toast.addToast({ this.$refs.toast.addToast({
icon: 'warning', icon: 'warning',
message: error.message, message: error.message,
@ -199,7 +195,7 @@ export default {
close() { close() {
if (this.bulkEditQuery) { if (this.bulkEditQuery) {
//TODO: deselect and save //TODO: deselect and save
this.premiseEditStore.deselectPremise(); // this.premiseEditStore.deselectPremise();
this.$router.push({name: 'bulk', params: {ids: this.bulkEditQuery}}); this.$router.push({name: 'bulk', params: {ids: this.bulkEditQuery}});
} else { } else {
//TODO: deselect and save //TODO: deselect and save
@ -210,20 +206,22 @@ export default {
let success = false; let success = false;
if (type === 'price') { if (type === 'price') {
success = await this.premiseEditStore.savePrice(); success = await this.premiseSingleEditStore.savePrice();
} else if (type === 'material') { } else if (type === 'material') {
success = await this.premiseEditStore.saveMaterial(); success = await this.premiseSingleEditStore.saveMaterial();
} else if (type === 'packaging') { } else if (type === 'packaging') {
success = await this.premiseEditStore.savePackaging(); success = await this.premiseSingleEditStore.savePackaging();
} }
}, if (!success)
updateMaterial(id, action) { this.$refs.toast.addToast({
console.log(id, action); icon: 'warning',
this.premiseEditStore.setMaterial(id, action === 'updateMasterData'); message: "Failed to save data.",
}, title: "Error saving",
updateSupplier(data) { variant: 'exception',
this.premiseEditStore.setSupplier(data.nodeId, data.updateMasterData); duration: 8000
});
}, },
trace() { trace() {
this.traceModal = true; this.traceModal = true;
@ -232,12 +230,11 @@ export default {
created() { created() {
[this.id] = new UrlSafeBase64().decodeIds(this.$route.params.id); [this.id] = new UrlSafeBase64().decodeIds(this.$route.params.id);
if (this.$route.params.ids) { if (this.$route.params.ids)
this.bulkEditQuery = this.$route.params.ids; this.bulkEditQuery = this.$route.params.ids;
this.premiseEditStore.selectSinglePremise(this.id, new UrlSafeBase64().decodeIds(this.$route.params.ids));
} else { this.premiseSingleEditStore.load(this.id)
this.premiseEditStore.loadAndSelectSinglePremise(this.id)
}
}, },
} }

View file

@ -0,0 +1,56 @@
import {defineStore} from 'pinia'
import {toRaw} from "vue";
export const useDestinationSingleEditStore = defineStore('destinationSingleEdit', {
state: () => ({
destination: null,
}),
actions: {
setDestination(from) {
const temp = {};
temp.id = `${from.id}`;
temp.destination_node = structuredClone(toRaw(from.destination_node));
temp.routes = structuredClone(toRaw(from.routes));
temp.annual_amount = from.annual_amount;
temp.is_d2d = from.is_d2d;
temp.rate_d2d = from.is_d2d ? from.rate_d2d : null;
temp.lead_time_d2d = from.is_d2d ? from.lead_time_d2d : null;
temp.handling_costs = from.handling_costs;
temp.disposal_costs = from.disposal_costs;
temp.repackaging_costs = from.repackaging_costs;
temp.userDefinedHandlingCosts = from.handling_costs !== null || from.disposal_costs !== null || from.repackaging_costs !== null;
this.destination = temp;
},
copyBack(to) {
to.id = parseInt(this.destination.id);
to.annual_amount = this.destination.annual_amount;
to.is_d2d = this.destination.is_d2d;
to.rate_d2d = this.destination.is_d2d ? this.destination.rate_d2d : null;
to.lead_time_d2d = this.destination.is_d2d ? this.destination.lead_time_d2d : null;
if (this.destination.userDefinedHandlingCosts) {
to.disposal_costs = this.destination.disposal_costs;
to.repackaging_costs = this.destination.repackaging_costs;
to.handling_costs = this.destination.handling_costs;
} else {
to.disposal_costs = null;
to.repackaging_costs = null;
to.handling_costs = null;
}
if ((this.destination.routes ?? null) !== null) {
to.routes.forEach(route => route.is_selected = this.destination.routes.find(r => r.id === route.id)?.is_selected ?? false);
}
return to;
}
},
});

View file

@ -242,32 +242,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
return state.premisses?.find(p => p.selected); return state.premisses?.find(p => p.selected);
}, },
/**
* Getters for destination editing
* ===============================
*/
getDestinationsView(state) {
return state.destinations?.destinations ?? [];
},
getDestinationById(state) {
return function (id) {
if (state.loading || state.selectedLoading) {
if (state.throwsException)
throw new Error("Premises are accessed while still loading.");
return null;
}
for (const p of state.premisses) {
const d = p.destinations.find(d => d.id === id);
if ((d ?? null) !== null) return d;
}
}
},
getSelectedDestinationsData: (state) => state.selectedDestination,
}, },
@ -658,59 +633,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
}, },
/**
* Set methods
* (these are more extensive changes. The edited premises are replaced by the one returned by the backend)
*/
async setSupplier(id, updateMasterData, ids = null) {
logger.info("setSupplier");
const selectedId = this.singleSelectId;
this.processDestinationMassEdit = true;
const body = {supplier_node_id: id, update_master_data: updateMasterData};
const url = `${config.backendUrl}/calculation/supplier/`;
await this.setData(url, body, ids);
if (selectedId != null && this.destinations && !this.destinations.fromMassEditView) {
this.prepareDestinations(selectedId, [selectedId]);
}
this.processDestinationMassEdit = false;
},
async setMaterial(id, updateMasterData, ids = null) {
logger.info("setMaterial");
const body = {material_id: id, update_master_data: updateMasterData};
const url = `${config.backendUrl}/calculation/material/`;
await this.setData(url, body, ids);
},
async setData(url, body, ids = null) {
const toBeUpdated = this.premisses ? (ids ? (ids) : (this.premisses.filter(p => p.selected).map(p => p.id))) : null;
if (null !== toBeUpdated) {
this.selectedLoading = true;
this.loading = true;
body.premise_id = toBeUpdated;
logger.info(url, body)
const {data: data} = await performRequest(this, 'PUT', url, body).catch(e => {
this.loading = false;
});
if (data) {
data.forEach(p => p.selected = true);
this.premisses = this.replacePremissesById(this.premisses, data);
}
this.loading = false;
this.selectedLoading = false;
}
},
/** /**
* Replace the premisses with the loaded ones by id. * Replace the premisses with the loaded ones by id.

View file

@ -0,0 +1,230 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import logger from "@/logger.js"
import performRequest from '@/backend.js'
export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', {
state() {
return {
premise: null,
loading: false,
calculating: false,
routing: false,
throwsException: true,
}
},
getters: {
showProcessingModal(state) {
return state.calculating || state.routing;
},
showLoadingSpinner(state) {
return state.loading;
},
isEmpty(state) {
return state.premise === null;
},
/**
* Getters for destination editing
* ===============================
*/
getDestinations(state) {
return state.premise?.destinations ?? [];
},
getDestinationById(state) {
return function (id) {
if (state.loading || state.selectedLoading) {
if (state.throwsException)
throw new Error("Premises are accessed while still loading.");
return null;
}
const d = state.premise.destinations.find(d => String(d.id) === String(id));
if ((d ?? null) !== null) return d;
}
}
},
actions: {
async load(id) {
this.loading = true;
this.premise = null;
const params = new URLSearchParams();
params.append('premissIds', id.toString());
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
const {data: data, headers: headers} = await performRequest(this, 'GET', url, null).catch(e => {
this.loading = false;
});
[this.premise] = data;
this.loading = false;
},
async startCalculation() {
this.calculating = true;
const body = this.premise?.id;
const url = `${config.backendUrl}/calculation/start/`;
let error = null;
await performRequest(this, 'PUT', url, body, false, ['Premiss validation error', 'Internal Server Error']).catch(e => {
logger.log("startCalculation exception", e.errorObj);
error = e.errorObj;
})
this.calculating = false;
return error;
},
/**
* Destination editing
*/
async addDestination(node) {
if (this.premise === null) return;
this.routing = true;
const body = {destination_node_id: node.id, premise_id: [this.premise.id]};
const url = `${config.backendUrl}/calculation/destination/`;
const {data: destinations} = await performRequest(this, 'POST', url, body).catch(e => {
this.routing = false;
throw e;
});
const ids = []
for (const destId of Object.keys(destinations)) {
this.premise.destinations.push(destinations[destId]);
ids.push(destinations[destId].id);
}
this.routing = false;
return ids;
},
async deleteDestination(id) {
if (this.premise === null || !this.premise.destinations.some(d => String(d.id) === String(id))) return;
const url = `${config.backendUrl}/calculation/destination/${id}`;
await performRequest(this, 'DELETE', url, null, false).catch(async e => {
logger.error("Unable to delete destination: " + id + "");
logger.error(e);
await this.load(this.premise.id);
});
const toBeDeleted = this.premise.destinations.findIndex(d => String(d.id) === String(id));
if (toBeDeleted !== -1) {
this.premise.destinations.splice(toBeDeleted, 1);
}
},
async updateDestination(id) {
if (this.premise === null) return;
const toUpdate = this.premise.destinations.find(to => String(id) === String(to.id));
const body = {
annual_amount: toUpdate.annual_amount,
repackaging_costs: toUpdate.repackaging_costs,
handling_costs: toUpdate.handling_costs,
disposal_costs: toUpdate.disposal_costs,
is_d2d: toUpdate.is_d2d,
rate_d2d: toUpdate.rate_d2d,
lead_time_d2d: toUpdate.lead_time_d2d,
route_selected_id: toUpdate.routes.find(r => r.is_selected)?.id ?? null,
};
logger.info(body)
const url = `${config.backendUrl}/calculation/destination/${toUpdate.id}`;
await performRequest(this, 'PUT', url, body, false);
},
/**
* Save
*/
async savePrice() {
let success = true;
if (!this.premise) return;
const body = {
premise_ids: [this.premise.id],
material_cost: this.premise.material_cost,
oversea_share: this.premise.oversea_share,
is_fca_enabled: this.premise.is_fca_enabled
};
await performRequest(this, 'POST', `${config.backendUrl}/calculation/price/`, body, false).catch(_ => {
success = false;
})
return success;
},
async savePackaging() {
let success = true;
if (!this.premise) return;
const body = {
premise_ids: [this.premise.id],
handling_unit: {
weight: this.premise.handling_unit.weight,
weight_unit: this.premise.handling_unit.weight_unit,
length: this.premise.handling_unit.length,
width: this.premise.handling_unit.width,
height: this.premise.handling_unit.height,
dimension_unit: this.premise.handling_unit.dimension_unit,
content_unit_count: this.premise.handling_unit.content_unit_count,
},
is_mixable: this.premise.is_mixable,
is_stackable: this.premise.is_stackable
};
await performRequest(this, 'POST', `${config.backendUrl}/calculation/packaging/`, body, false).catch(() => {
success = false;
})
return success;
},
async saveMaterial() {
let success = true;
if (!this.premise) return;
const body = {
premise_ids: [this.premise.id],
hs_code: this.premise.hs_code,
tariff_rate: this.premise.tariff_rate,
};
await performRequest(this, 'POST', `${config.backendUrl}/calculation/material/`, body, false).catch(() => {
success = false;
})
return success;
},
}
});

View file

@ -8,7 +8,6 @@ import de.avatic.lcc.dto.calculation.ResolvePremiseDTO;
import de.avatic.lcc.dto.calculation.create.CreatePremiseDTO; import de.avatic.lcc.dto.calculation.create.CreatePremiseDTO;
import de.avatic.lcc.dto.calculation.create.PremiseSearchResultDTO; import de.avatic.lcc.dto.calculation.create.PremiseSearchResultDTO;
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
import de.avatic.lcc.dto.calculation.edit.SetDataDTO;
import de.avatic.lcc.dto.calculation.edit.destination.DestinationCreateDTO; import de.avatic.lcc.dto.calculation.edit.destination.DestinationCreateDTO;
import de.avatic.lcc.dto.calculation.edit.destination.DestinationSetDTO; import de.avatic.lcc.dto.calculation.edit.destination.DestinationSetDTO;
import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO; import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO;
@ -17,8 +16,6 @@ import de.avatic.lcc.dto.calculation.edit.masterData.PackagingUpdateDTO;
import de.avatic.lcc.dto.calculation.edit.masterData.PriceUpdateDTO; import de.avatic.lcc.dto.calculation.edit.masterData.PriceUpdateDTO;
import de.avatic.lcc.service.access.DestinationService; import de.avatic.lcc.service.access.DestinationService;
import de.avatic.lcc.service.access.PremisesService; import de.avatic.lcc.service.access.PremisesService;
import de.avatic.lcc.service.calculation.ChangeMaterialService;
import de.avatic.lcc.service.calculation.ChangeSupplierService;
import de.avatic.lcc.service.calculation.PremiseCreationService; import de.avatic.lcc.service.calculation.PremiseCreationService;
import de.avatic.lcc.service.calculation.PremiseSearchStringAnalyzerService; import de.avatic.lcc.service.calculation.PremiseSearchStringAnalyzerService;
import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException; import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
@ -44,20 +41,17 @@ import java.util.Map;
public class PremiseController { public class PremiseController {
private static final Logger log = LoggerFactory.getLogger(PremiseController.class); private static final Logger log = LoggerFactory.getLogger(PremiseController.class);
private final PremiseSearchStringAnalyzerService premiseSearchStringAnalyzerService; private final PremiseSearchStringAnalyzerService premiseSearchStringAnalyzerService;
private final PremisesService premisesServices; private final PremisesService premisesServices;
private final PremiseCreationService premiseCreationService; private final PremiseCreationService premiseCreationService;
private final DestinationService destinationService; private final DestinationService destinationService;
private final ChangeSupplierService changeSupplierService;
private final ChangeMaterialService changeMaterialService;
public PremiseController(PremiseSearchStringAnalyzerService premiseSearchStringAnalyzerService, PremisesService premisesServices, PremiseCreationService premiseCreationService, DestinationService destinationService, ChangeSupplierService changeSupplierService, ChangeMaterialService changeMaterialService) { public PremiseController(PremiseSearchStringAnalyzerService premiseSearchStringAnalyzerService, PremisesService premisesServices, PremiseCreationService premiseCreationService, DestinationService destinationService) {
this.premiseSearchStringAnalyzerService = premiseSearchStringAnalyzerService; this.premiseSearchStringAnalyzerService = premiseSearchStringAnalyzerService;
this.premisesServices = premisesServices; this.premisesServices = premisesServices;
this.premiseCreationService = premiseCreationService; this.premiseCreationService = premiseCreationService;
this.destinationService = destinationService; this.destinationService = destinationService;
this.changeSupplierService = changeSupplierService;
this.changeMaterialService = changeMaterialService;
} }
@GetMapping({"/view", "/view/"}) @GetMapping({"/view", "/view/"})
@ -213,16 +207,6 @@ public class PremiseController {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@PutMapping({"/supplier", "/supplier/"})
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity<List<PremiseDetailDTO>> setSupplier(@RequestBody SetDataDTO setSupplierDTO) {
return ResponseEntity.ok(changeSupplierService.setSupplier(setSupplierDTO));
}
@PutMapping({"/material", "/material/"})
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity<List<PremiseDetailDTO>> setMaterial(@RequestBody SetDataDTO setMaterialDTO) {
return ResponseEntity.ok(changeMaterialService.setMaterial(setMaterialDTO));
}
} }

View file

@ -1,58 +0,0 @@
package de.avatic.lcc.controller.custom;
import de.avatic.lcc.dto.custom.CustomDTO;
import de.avatic.lcc.model.taric.Nomenclature;
import de.avatic.lcc.service.api.CustomApiService;
import de.avatic.lcc.service.api.EUTaxationApiWrapperService;
import de.avatic.lcc.service.calculation.NomenclatureService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* Controller for handling custom tariff related requests.
* Provides endpoints for retrieving tariff rates based on HS code and country ID.
*/
@RestController
@RequestMapping("/api/customs")
public class CustomController {
private final CustomApiService customApiService;
private final NomenclatureService nomenclatureService;
private final EUTaxationApiWrapperService eUTaxationApiWrapperService;
/**
* Constructs a new instance of CustomController with the given service.
*
* @param customApiService the service responsible for custom tariff calculations
*/
public CustomController(CustomApiService customApiService, NomenclatureService nomenclatureService, EUTaxationApiWrapperService eUTaxationApiWrapperService) {
this.customApiService = customApiService;
this.nomenclatureService = nomenclatureService;
this.eUTaxationApiWrapperService = eUTaxationApiWrapperService;
}
/**
* Retrieves the tariff rate for the specified HS code and country ID.
*
* @param hsCode the HS code representing the product classification
* @param countryIds the ID of the country for which the tariff rate is required
* @return a {@code ResponseEntity} containing the tariff rate as a {@code Number}
*/
@GetMapping({"/",""})
public ResponseEntity<List<CustomDTO>> getTariffRate(@RequestParam(value = "hs_code") String hsCode, @RequestParam(value = "country_ids") List<Integer> countryIds) {
var res = eUTaxationApiWrapperService.getTariffRates(hsCode, countryIds);
return ResponseEntity.ok(res);
}
@GetMapping({"/search", "/search/"})
public ResponseEntity<List<String>> getNomenclature(@RequestParam(value = "hs_code") String hsCode) {
return ResponseEntity.ok(nomenclatureService.getNomenclature(hsCode));
}
}

View file

@ -43,6 +43,9 @@ public class PremiseDetailDTO {
@JsonProperty("is_fca_enabled") @JsonProperty("is_fca_enabled")
private Boolean isFcaEnabled; private Boolean isFcaEnabled;
@JsonProperty("tariff_unlocked")
private Boolean tariffUnlocked;
public Double getMaterialCost() { public Double getMaterialCost() {
return materialCost; return materialCost;
@ -142,4 +145,13 @@ public class PremiseDetailDTO {
public void setDestinations(List<DestinationDTO> destinations) { public void setDestinations(List<DestinationDTO> destinations) {
this.destinations = destinations; this.destinations = destinations;
} }
public void setTariffUnlocked(Boolean tariffUnlocked) {
this.tariffUnlocked = tariffUnlocked;
}
@JsonIgnore
public Boolean getTariffUnlocked() {
return tariffUnlocked;
}
} }

View file

@ -22,9 +22,6 @@ public class MaterialUpdateDTO {
@Digits(integer = 4, fraction = 4, message = "Tariff rate must have at most 4 decimal places") @Digits(integer = 4, fraction = 4, message = "Tariff rate must have at most 4 decimal places")
private Number tariffRate; private Number tariffRate;
@JsonProperty("tariff_rates")
private Map<Integer, Number> tariffRates;
public String getHsCode() { public String getHsCode() {
return hsCode; return hsCode;
} }
@ -49,11 +46,4 @@ public class MaterialUpdateDTO {
this.premiseIds = premiseIds; this.premiseIds = premiseIds;
} }
public Map<Integer, Number> getTariffRates() {
return tariffRates;
}
public void setTariffRates(Map<Integer, Number> tariffRates) {
this.tariffRates = tariffRates;
}
} }

View file

@ -36,6 +36,8 @@ public class Premise {
@Digits(integer = 7, fraction = 2) @Digits(integer = 7, fraction = 2)
private BigDecimal tariffRate; private BigDecimal tariffRate;
private Boolean tariffUnlocked;
@Size(max = 16) @Size(max = 16)
private PremiseState state; private PremiseState state;
@ -276,4 +278,12 @@ public class Premise {
public void setUserId(Integer userId) { public void setUserId(Integer userId) {
this.userId = userId; this.userId = userId;
} }
public Boolean getTariffUnlocked() {
return tariffUnlocked;
}
public void setTariffUnlocked(Boolean tariffUnlocked) {
this.tariffUnlocked = tariffUnlocked;
}
} }

View file

@ -1,4 +1,4 @@
package de.avatic.lcc.model.custom; package de.avatic.lcc.model.eutaxation;
import java.util.Optional; import java.util.Optional;

View file

@ -0,0 +1,67 @@
package de.avatic.lcc.model.zolltarifnummern;
import java.lang.reflect.Array;
import java.util.List;
public class ZolltarifnummernResponse {
String query;
String year;
String lang;
String version;
String total;
List<ZolltarifnummernResponseEntry> suggestions;
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
public String getYear() {
return year;
}
public void setYear(String year) {
this.year = year;
}
public String getLang() {
return lang;
}
public void setLang(String lang) {
this.lang = lang;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getTotal() {
return total;
}
public void setTotal(String total) {
this.total = total;
}
public List<ZolltarifnummernResponseEntry> getSuggestions() {
return suggestions;
}
public void setSuggestions(List<ZolltarifnummernResponseEntry> suggestions) {
this.suggestions = suggestions;
}
}

View file

@ -0,0 +1,44 @@
package de.avatic.lcc.model.zolltarifnummern;
public class ZolltarifnummernResponseEntry {
String code;
String score;
String value;
String data;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getScore() {
return score;
}
public void setScore(String score) {
this.score = score;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}

View file

@ -8,6 +8,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
@ -23,10 +24,15 @@ public class DistanceMatrixRepository {
this.jdbcTemplate = jdbcTemplate; this.jdbcTemplate = jdbcTemplate;
} }
@Transactional
public Optional<Distance> getDistance(Node src, boolean isUsrFrom, Node dest, boolean isUsrTo) { public Optional<Distance> getDistance(Node src, boolean isUsrFrom, Node dest, boolean isUsrTo) {
String query = "SELECT * FROM distance_matrix WHERE ? = ? AND ? = ? AND state = ?"; String fromCol = isUsrFrom ? "from_user_node_id" : "from_node_id";
String toCol = isUsrTo ? "to_user_node_id" : "to_node_id";
var distance = jdbcTemplate.query(query, new DistanceMapper(), isUsrFrom ? "from_user_node_id" : "from_node_id", src.getId(), isUsrTo ? "to_user_node_id" : "to_node_id", dest.getId(), DistanceMatrixState.VALID.name()); String query = "SELECT * FROM distance_matrix WHERE " + fromCol + " = ? AND " + toCol + " = ? AND state = ?";
var distance = jdbcTemplate.query(query, new DistanceMapper(),
src.getId(), dest.getId(), DistanceMatrixState.VALID.name());
if (distance.isEmpty()) if (distance.isEmpty())
return Optional.empty(); return Optional.empty();
@ -34,31 +40,35 @@ public class DistanceMatrixRepository {
return Optional.of(distance.getFirst()); return Optional.of(distance.getFirst());
} }
@Transactional
public void saveDistance(Distance distance) { public void saveDistance(Distance distance) {
try { try {
// Determine which columns to use
String fromCol = distance.getFromUserNodeId() != null ? "from_user_node_id" : "from_node_id";
String toCol = distance.getToUserNodeId() != null ? "to_user_node_id" : "to_node_id";
Integer fromId = distance.getFromUserNodeId() != null ? distance.getFromUserNodeId() : distance.getFromNodeId();
Integer toId = distance.getToUserNodeId() != null ? distance.getToUserNodeId() : distance.getToNodeId();
// First, check if an entry already exists // First, check if an entry already exists
String checkQuery = "SELECT id FROM distance_matrix WHERE ? = ? AND ? = ?"; String checkQuery = "SELECT id FROM distance_matrix WHERE " + fromCol + " = ? AND " + toCol + " = ?";
var existingIds = jdbcTemplate.query(checkQuery, var existingIds = jdbcTemplate.query(checkQuery,
(rs, rowNum) -> rs.getInt("id"), (rs, rowNum) -> rs.getInt("id"),
distance.getFromUserNodeId() != null ? "from_user_node_id" : "from_node_id", fromId,
distance.getFromUserNodeId() != null ? distance.getFromUserNodeId() : distance.getFromNodeId(), toId);
distance.getToUserNodeId() != null ? "to_user_node_id" : "to_node_id",
distance.getToUserNodeId() != null ? distance.getToUserNodeId() : distance.getToNodeId());
if (!existingIds.isEmpty()) { if (!existingIds.isEmpty()) {
// Update existing entry // Update existing entry
String updateQuery = """ String updateQuery = """
UPDATE distance_matrix UPDATE distance_matrix
SET from_geo_lat = ?, SET from_geo_lat = ?,
from_geo_lng = ?, from_geo_lng = ?,
to_geo_lat = ?, to_geo_lat = ?,
to_geo_lng = ?, to_geo_lng = ?,
distance = ?, distance = ?,
state = ?, state = ?,
updated_at = ? updated_at = ?
WHERE ? = ? AND ? = ? WHERE\s""" + fromCol + " = ? AND " + toCol + " = ?";
""";
jdbcTemplate.update(updateQuery, jdbcTemplate.update(updateQuery,
distance.getFromGeoLat(), distance.getFromGeoLat(),
@ -68,20 +78,18 @@ public class DistanceMatrixRepository {
distance.getDistance(), distance.getDistance(),
distance.getState().name(), distance.getState().name(),
distance.getUpdatedAt(), distance.getUpdatedAt(),
distance.getFromUserNodeId() != null ? "from_user_node_id" : "from_node_id", fromId,
distance.getFromUserNodeId() != null ? distance.getFromUserNodeId() : distance.getFromNodeId(), toId);
distance.getToUserNodeId() != null ? "to_user_node_id" : "to_node_id",
distance.getToUserNodeId() != null ? distance.getToUserNodeId() : distance.getToNodeId());
logger.info("Updated existing distance entry for nodes {} -> {}", logger.info("Updated existing distance entry for nodes {} -> {}",
distance.getFromNodeId(), distance.getToNodeId()); distance.getFromNodeId(), distance.getToNodeId());
} else { } else {
// Insert new entry // Insert new entry
String insertQuery = """ String insertQuery = """
INSERT INTO distance_matrix INSERT INTO distance_matrix
(from_node_id, to_node_id, from_user_node_id, to_user_node_id, from_geo_lat, from_geo_lng, to_geo_lat, to_geo_lng, distance, state, updated_at) (from_node_id, to_node_id, from_user_node_id, to_user_node_id, from_geo_lat, from_geo_lng, to_geo_lat, to_geo_lng, distance, state, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""; """;
jdbcTemplate.update(insertQuery, jdbcTemplate.update(insertQuery,
distance.getFromNodeId(), distance.getFromNodeId(),

View file

@ -1,37 +0,0 @@
package de.avatic.lcc.repositories;
import de.avatic.lcc.service.api.EUTaxationApiService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class NomenclatureRepository {
private final JdbcTemplate jdbcTemplate;
private final EUTaxationApiService eUTaxationApiService;
public NomenclatureRepository(JdbcTemplate jdbcTemplate, EUTaxationApiService eUTaxationApiService) {
this.jdbcTemplate = jdbcTemplate;
this.eUTaxationApiService = eUTaxationApiService;
}
public List<String> searchHsCode(String search) {
String sql = """
SELECT hs_code FROM nomenclature WHERE hs_code LIKE CONCAT(?, '%') LIMIT 10
""";
return jdbcTemplate.queryForList (sql, String.class, search);
}
public boolean validate(String hsCode) {
try {
var foundNomenclature = eUTaxationApiService.getGoodsDescription(hsCode, "en");
return foundNomenclature.getReturn().getResult().getData().isDeclarable();
} catch (Exception e) {
return false;
}
}
}

View file

@ -285,6 +285,11 @@ public class PremiseRepository {
@Transactional @Transactional
public void updateMaterial(List<Integer> premiseIds, String hsCode, BigDecimal tariffRate) { public void updateMaterial(List<Integer> premiseIds, String hsCode, BigDecimal tariffRate) {
updateMaterial(premiseIds, hsCode, tariffRate, null);
}
@Transactional
public void updateMaterial(List<Integer> premiseIds, String hsCode, BigDecimal tariffRate, Boolean tariffUnlocked) {
// Build the SET clause dynamically based on non-null parameters // Build the SET clause dynamically based on non-null parameters
List<String> setClauses = new ArrayList<>(); List<String> setClauses = new ArrayList<>();
@ -300,6 +305,11 @@ public class PremiseRepository {
parameters.add(tariffRate); parameters.add(tariffRate);
} }
if (tariffUnlocked != null) {
setClauses.add("tariff_unlocked = ?");
parameters.add(tariffUnlocked);
}
// If no fields to update, return early // If no fields to update, return early
if (setClauses.isEmpty()) { if (setClauses.isEmpty()) {
return; return;
@ -325,6 +335,8 @@ public class PremiseRepository {
ps.setBigDecimal(i + 1, (BigDecimal) param); ps.setBigDecimal(i + 1, (BigDecimal) param);
} else if (param instanceof Integer) { } else if (param instanceof Integer) {
ps.setInt(i + 1, (Integer) param); ps.setInt(i + 1, (Integer) param);
} else if(param instanceof Boolean) {
ps.setBoolean(i + 1, (Boolean) param);
} }
} }
} }
@ -669,6 +681,24 @@ public class PremiseRepository {
} }
@Transactional
public List<Integer> getIdsWithUnlockedTariffs(List<Integer> premiseIds) {
if (premiseIds == null || premiseIds.isEmpty()) {
return premiseIds;
}
String sql = "SELECT id FROM premise WHERE id IN (:ids) AND tariff_unlocked = TRUE";
List<Integer> unlockedIds = namedParameterJdbcTemplate.query(
sql,
new MapSqlParameterSource("ids", premiseIds),
(rs, rowNum) -> rs.getInt("id")
);
return unlockedIds;
}
/** /**
* Encapsulates SQL query building logic * Encapsulates SQL query building logic
*/ */
@ -888,6 +918,8 @@ public class PremiseRepository {
entity.setTariffRate(rs.getBigDecimal("tariff_rate")); entity.setTariffRate(rs.getBigDecimal("tariff_rate"));
entity.setTariffUnlocked(rs.getBoolean("tariff_unlocked"));
entity.setFcaEnabled(rs.getBoolean("is_fca_enabled")); entity.setFcaEnabled(rs.getBoolean("is_fca_enabled"));
if (rs.wasNull()) if (rs.wasNull())
entity.setFcaEnabled(null); entity.setFcaEnabled(null);

View file

@ -211,15 +211,11 @@ public class PremisesService {
premiseRepository.checkOwner(materialUpdateDTO.getPremiseIds(), userId); premiseRepository.checkOwner(materialUpdateDTO.getPremiseIds(), userId);
if(materialUpdateDTO.getTariffRates() != null) {
var rates = materialUpdateDTO.getTariffRates();
rates.keySet().forEach(id -> premiseRepository.updateMaterial(List.of(id), materialUpdateDTO.getHsCode(), rates.get(id) == null ? null : BigDecimal.valueOf(rates.get(id).doubleValue())));
return;
}
var unlockedIds = premiseRepository.getIdsWithUnlockedTariffs(materialUpdateDTO.getPremiseIds());
var tariffRate = materialUpdateDTO.getTariffRate() == null ? null : BigDecimal.valueOf(materialUpdateDTO.getTariffRate().doubleValue()); var tariffRate = materialUpdateDTO.getTariffRate() == null ? null : BigDecimal.valueOf(materialUpdateDTO.getTariffRate().doubleValue());
premiseRepository.updateMaterial(materialUpdateDTO.getPremiseIds(), materialUpdateDTO.getHsCode(), tariffRate); premiseRepository.updateMaterial(unlockedIds, materialUpdateDTO.getHsCode(), tariffRate);
} }
@ -305,7 +301,7 @@ public class PremisesService {
var old = premiseRepository.getPremiseById(id).orElseThrow(); var old = premiseRepository.getPremiseById(id).orElseThrow();
var newId = premiseRepository.insert(old.getMaterialId(), old.getSupplierNodeId(), old.getUserSupplierNodeId(), BigDecimal.valueOf(old.getLocation().getLatitude()), BigDecimal.valueOf(old.getLocation().getLongitude()), old.getCountryId(), userId); var newId = premiseRepository.insert(old.getMaterialId(), old.getSupplierNodeId(), old.getUserSupplierNodeId(), BigDecimal.valueOf(old.getLocation().getLatitude()), BigDecimal.valueOf(old.getLocation().getLongitude()), old.getCountryId(), userId);
premiseRepository.updateMaterial(Collections.singletonList(newId), old.getHsCode(), old.getTariffRate()); premiseRepository.updateMaterial(Collections.singletonList(newId), old.getHsCode(), old.getTariffRate(), old.getTariffUnlocked());
premiseRepository.updatePrice(Collections.singletonList(newId), old.getMaterialCost(), old.getFcaEnabled(), old.getOverseaShare()); premiseRepository.updatePrice(Collections.singletonList(newId), old.getMaterialCost(), old.getFcaEnabled(), old.getOverseaShare());
premiseRepository.updatePackaging(Collections.singletonList(newId), dimensionTransformer.toDimensionEntity(old), old.getHuStackable(), old.getHuMixable()); premiseRepository.updatePackaging(Collections.singletonList(newId), dimensionTransformer.toDimensionEntity(old), old.getHuStackable(), old.getHuMixable());
premiseRepository.setPackagingId(newId, old.getPackagingId()); premiseRepository.setPackagingId(newId, old.getPackagingId());

View file

@ -122,7 +122,7 @@ public class DistanceApiService {
distance.setToNodeId(null); distance.setToNodeId(null);
} else { } else {
distance.setToUserNodeId(null); distance.setToUserNodeId(null);
distance.setToNodeId(from.getId()); distance.setToNodeId(to.getId());
} }
distance.setFromGeoLat(from.getGeoLat()); distance.setFromGeoLat(from.getGeoLat());

View file

@ -2,6 +2,7 @@ package de.avatic.lcc.service.api;
import eu.europa.ec.taxation.taric.client.*; import eu.europa.ec.taxation.taric.client.*;
import jakarta.xml.bind.JAXBElement; import jakarta.xml.bind.JAXBElement;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.ws.client.core.WebServiceTemplate; import org.springframework.ws.client.core.WebServiceTemplate;
@ -9,6 +10,7 @@ import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.datatype.XMLGregorianCalendar;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.concurrent.CompletableFuture;
@Service @Service
public class EUTaxationApiService { public class EUTaxationApiService {
@ -21,7 +23,8 @@ public class EUTaxationApiService {
this.objectFactory = new ObjectFactory(); this.objectFactory = new ObjectFactory();
} }
public GoodsDescrForWsResponse getGoodsDescription(String goodsCode, String languageCode) { @Async("customLookupExecutor")
public CompletableFuture<GoodsDescrForWsResponse> getGoodsDescription(String goodsCode, String languageCode) {
GoodsDescrForWs request = new GoodsDescrForWs(); GoodsDescrForWs request = new GoodsDescrForWs();
request.setGoodsCode(goodsCode); request.setGoodsCode(goodsCode);
request.setLanguageCode(languageCode); request.setLanguageCode(languageCode);
@ -33,10 +36,11 @@ public class EUTaxationApiService {
JAXBElement<GoodsDescrForWsResponse> responseElement = JAXBElement<GoodsDescrForWsResponse> responseElement =
(JAXBElement<GoodsDescrForWsResponse>) webServiceTemplate.marshalSendAndReceive(requestElement); (JAXBElement<GoodsDescrForWsResponse>) webServiceTemplate.marshalSendAndReceive(requestElement);
return responseElement.getValue(); return CompletableFuture.completedFuture(responseElement.getValue());
} }
public GoodsMeasForWsResponse getGoodsMeasures(String goodsCode, String countryCode, String tradeMovement) { @Async("customLookupExecutor")
public CompletableFuture<GoodsMeasForWsResponse> getGoodsMeasures(String goodsCode, String countryCode, String tradeMovement) {
GoodsMeasForWs request = new GoodsMeasForWs(); GoodsMeasForWs request = new GoodsMeasForWs();
request.setGoodsCode(goodsCode); request.setGoodsCode(goodsCode);
request.setCountryCode(countryCode); request.setCountryCode(countryCode);
@ -49,7 +53,7 @@ public class EUTaxationApiService {
JAXBElement<GoodsMeasForWsResponse> responseElement = JAXBElement<GoodsMeasForWsResponse> responseElement =
(JAXBElement<GoodsMeasForWsResponse>) webServiceTemplate.marshalSendAndReceive(requestElement); (JAXBElement<GoodsMeasForWsResponse>) webServiceTemplate.marshalSendAndReceive(requestElement);
return responseElement.getValue(); return CompletableFuture.completedFuture(responseElement.getValue());
} }
private XMLGregorianCalendar getCurrentDate() { private XMLGregorianCalendar getCurrentDate() {

View file

@ -1,182 +0,0 @@
package de.avatic.lcc.service.api;
import de.avatic.lcc.dto.custom.CustomDTO;
import de.avatic.lcc.dto.custom.CustomMeasureDTO;
import de.avatic.lcc.dto.generic.PropertyDTO;
import de.avatic.lcc.model.custom.MeasureType;
import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.util.exception.base.InternalErrorException;
import eu.europa.ec.taxation.taric.client.GoodsMeasuresForWsResponse;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@Service
public class EUTaxationApiWrapperService {
private final CountryRepository countryRepository;
private final EUTaxationApiService eUTaxationApiService;
private final PropertyRepository propertyRepository;
public EUTaxationApiWrapperService(CountryRepository countryRepository, EUTaxationApiService eUTaxationApiService, PropertyRepository propertyRepository) {
this.countryRepository = countryRepository;
this.eUTaxationApiService = eUTaxationApiService;
this.propertyRepository = propertyRepository;
}
public boolean validate(String hsCode) {
try {
var goodsDescription = eUTaxationApiService.getGoodsDescription(hsCode, "en");
return goodsDescription.getReturn().getResult().getData().isDeclarable() == true;
} catch (Exception e) {
// just continue
}
return false;
}
public List<CustomDTO> getTariffRates(String hsCode, List<Integer> countryId) {
var futures = countryId.stream().map(country -> getTariffRate(hsCode, country)).toList();
return futures.stream().map(CompletableFuture::join).toList();
}
public CustomDTO getTariffRateImmediate(String hsCode, Integer countryId) {
var country = countryRepository.getById(countryId);
String iso = country.orElseThrow().getIsoCode().name();
List<CustomMeasureDTO> customMeasures = new ArrayList<>();
try {
var measForWsResponse = eUTaxationApiService.getGoodsMeasures(hsCode, iso.toUpperCase(), "I");
GoodsMeasuresForWsResponse.Measures.Measure selectedMeasure = null;
Double selectedDuty = null;
int rank = Integer.MAX_VALUE;
var measures = filterToNewestMeasuresPerType(measForWsResponse.getReturn().getResult().getMeasures().getMeasure());
for (var measure : measures) {
var measureType = MeasureType.fromMeasureCode(measure.getMeasureType().getMeasureType());
boolean maybeRelevant = measureType.map(MeasureType::containsRelevantDuty).orElse(false);
if (maybeRelevant) {
var duty = extractDuty(measure);
if (rank > measureType.get().ordinal() && duty.isPresent()) {
rank = measureType.get().ordinal();
selectedDuty = duty.get();
selectedMeasure = measure;
}
}
customMeasures.add(new CustomMeasureDTO(
measure.getMeasureType().getMeasureType(),
measure.getMeasureType().getDescription(),
measure.getRegulationId(),
measure.getDutyRate()));
}
if (selectedDuty != null) {
return new CustomDTO(false, selectedDuty, customMeasures, countryId);
}
} catch (Exception e) {
// just continue
}
return propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.TARIFF_RATE).map(PropertyDTO::getCurrentValue).map(Double::valueOf).map(d -> new CustomDTO(d, customMeasures, countryId)).orElseThrow(() -> new InternalErrorException("Unable to load default custom rate. Please contact support."));
}
@Async("customLookupExecutor")
public CompletableFuture<CustomDTO> getTariffRate(String hsCode, Integer countryId) {
var country = countryRepository.getById(countryId);
String iso = country.orElseThrow().getIsoCode().name();
List<CustomMeasureDTO> customMeasures = new ArrayList<>();
try {
var measForWsResponse = eUTaxationApiService.getGoodsMeasures(hsCode, iso.toUpperCase(), "I");
GoodsMeasuresForWsResponse.Measures.Measure selectedMeasure = null;
Double selectedDuty = null;
int rank = Integer.MAX_VALUE;
var measures = filterToNewestMeasuresPerType(measForWsResponse.getReturn().getResult().getMeasures().getMeasure());
for (var measure : measures) {
var measureType = MeasureType.fromMeasureCode(measure.getMeasureType().getMeasureType());
boolean maybeRelevant = measureType.map(MeasureType::containsRelevantDuty).orElse(false);
if (maybeRelevant) {
var duty = extractDuty(measure);
if (rank > measureType.get().ordinal() && duty.isPresent()) {
rank = measureType.get().ordinal();
selectedDuty = duty.get();
selectedMeasure = measure;
}
}
customMeasures.add(new CustomMeasureDTO(
measure.getMeasureType().getMeasureType(),
measure.getMeasureType().getDescription(),
measure.getRegulationId(),
measure.getDutyRate()));
}
if (selectedDuty != null) {
return CompletableFuture.completedFuture(new CustomDTO(false, selectedDuty, customMeasures, countryId));
}
} catch (Exception e) {
// just continue
}
return CompletableFuture.completedFuture(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.TARIFF_RATE).map(PropertyDTO::getCurrentValue).map(Double::valueOf).map(d -> new CustomDTO(d, customMeasures, countryId)).orElseThrow(() -> new InternalErrorException("Unable to load default custom rate. Please contact support.")));
}
public List<GoodsMeasuresForWsResponse.Measures.Measure> filterToNewestMeasuresPerType(List<GoodsMeasuresForWsResponse.Measures.Measure> measures) {
Map<String, GoodsMeasuresForWsResponse.Measures.Measure> newestByType = new HashMap<>();
for (GoodsMeasuresForWsResponse.Measures.Measure measure : measures) {
String measureTypeKey = measure.getMeasureType().getMeasureType();
GoodsMeasuresForWsResponse.Measures.Measure existing = newestByType.get(measureTypeKey);
if (existing == null ||
measure.getValidityStartDate().compare(existing.getValidityStartDate()) > 0) {
newestByType.put(measureTypeKey, measure);
}
}
return new ArrayList<>(newestByType.values());
}
private Optional<Double> extractDuty(GoodsMeasuresForWsResponse.Measures.Measure measure) {
var dutyRate = measure.getDutyRate();
if (dutyRate == null) return Optional.empty();
if (dutyRate.trim().matches("\\d+\\.\\d+\\s*%")) {
return Optional.of(Double.parseDouble(dutyRate.trim().replace("%", "").trim()) / 100);
}
return Optional.empty();
}
}

View file

@ -0,0 +1,264 @@
package de.avatic.lcc.service.api;
import de.avatic.lcc.dto.custom.CustomDTO;
import de.avatic.lcc.dto.custom.CustomMeasureDTO;
import de.avatic.lcc.dto.generic.PropertyDTO;
import de.avatic.lcc.model.db.country.Country;
import de.avatic.lcc.model.db.materials.Material;
import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.model.eutaxation.MeasureType;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.util.exception.base.InternalErrorException;
import eu.europa.ec.taxation.taric.client.GoodsMeasForWsResponse;
import eu.europa.ec.taxation.taric.client.GoodsMeasuresForWsResponse;
import jakarta.annotation.Nullable;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
public class TaxationResolverService {
private final CountryRepository countryRepository;
private final EUTaxationApiService eUTaxationApiService;
private final PropertyRepository propertyRepository;
private final ZolltarifnummernApiService zolltarifnummernApiService;
public TaxationResolverService(CountryRepository countryRepository, EUTaxationApiService eUTaxationApiService, PropertyRepository propertyRepository, ZolltarifnummernApiService zolltarifnummernApiService) {
this.countryRepository = countryRepository;
this.eUTaxationApiService = eUTaxationApiService;
this.propertyRepository = propertyRepository;
this.zolltarifnummernApiService = zolltarifnummernApiService;
}
private Map<TaxationResolverRequest, List<GoodsMeasForWsResponse>> doRequests(List<TaxationResolverRequest> requests) {
var filteredRequests = requests.stream().collect(Collectors.partitioningBy(r -> r.material().getHsCode() != null && r.material().getHsCode().length() < 10));
var joined = Stream.concat(
filteredRequests.get(false).stream()
.filter(r -> r.material().getHsCode() != null)
.map(r -> new TaxationResolverSingleRequest(r.material().getHsCode(), r.countryId(), r)),
resolveIncompleteHsCodes(filteredRequests.get(true)).stream());
var singleResponses = doSingleRequests(joined.toList());
return requests.stream().collect(Collectors.toMap(
r -> r,
r -> singleResponses.keySet().stream().filter(k -> k.origin.equals(r)).map(singleResponses::get).toList()
));
}
private List<TaxationResolverSingleRequest> resolveIncompleteHsCodes(List<TaxationResolverRequest> request) {
var futures = request.stream()
.collect(Collectors.toMap(
r -> r,
r -> zolltarifnummernApiService.getDeclarableHsCodes(r.material().getHsCode()))
);
CompletableFuture.allOf(futures.values().toArray(new CompletableFuture[0])).join();
return futures.keySet().stream().flatMap(k -> futures.get(k).join().stream().map(resp -> new TaxationResolverSingleRequest(resp, k.countryId(), k))).toList();
}
private Map<TaxationResolverSingleRequest, GoodsMeasForWsResponse> doSingleRequests(List<TaxationResolverSingleRequest> requests) {
Map<Integer, Country> countries = requests.stream().collect(Collectors.toMap(TaxationResolverSingleRequest::countryId, r -> countryRepository.getById(r.countryId()).orElseThrow()));
Map<TaxationResolverSingleRequest, CompletableFuture<GoodsMeasForWsResponse>> futureMap =
requests.stream()
.collect(Collectors.toMap(
r -> r,
r -> eUTaxationApiService.getGoodsMeasures(
r.hsCode,
countries.get(r.countryId()).getIsoCode().getCode(),
"I"
)
));
CompletableFuture.allOf(futureMap.values().toArray(new CompletableFuture[0])).join();
return
futureMap.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().join()
));
}
private Optional<Double> extractDuty(GoodsMeasuresForWsResponse.Measures.Measure measure) {
var dutyRate = measure.getDutyRate();
if (dutyRate == null) return Optional.empty();
if (dutyRate.trim().matches("\\d+\\.\\d+\\s*%")) {
return Optional.of(Double.parseDouble(dutyRate.trim().replace("%", "").trim()) / 100);
}
return Optional.empty();
}
private List<GoodsMeasuresForWsResponse.Measures.Measure> filterToNewestMeasuresPerType(List<GoodsMeasuresForWsResponse.Measures.Measure> measures) {
Map<String, GoodsMeasuresForWsResponse.Measures.Measure> newestByType = new HashMap<>();
for (GoodsMeasuresForWsResponse.Measures.Measure measure : measures) {
String measureTypeKey = measure.getMeasureType().getMeasureType();
GoodsMeasuresForWsResponse.Measures.Measure existing = newestByType.get(measureTypeKey);
if (existing == null ||
measure.getValidityStartDate().compare(existing.getValidityStartDate()) > 0) {
newestByType.put(measureTypeKey, measure);
}
}
return new ArrayList<>(newestByType.values());
}
public boolean validate(String hsCode) {
try {
var goodsDescription = eUTaxationApiService.getGoodsDescription(hsCode, "en").join();
return goodsDescription.getReturn().getResult().getData().isDeclarable() == true;
} catch (Exception e) {
// just continue
}
return false;
}
public CustomDTO getTariffRateImmediate(String hsCode, Integer countryId) {
var country = countryRepository.getById(countryId);
String iso = country.orElseThrow().getIsoCode().name();
List<CustomMeasureDTO> customMeasures = new ArrayList<>();
try {
var measForWsResponse = eUTaxationApiService.getGoodsMeasures(hsCode, iso.toUpperCase(), "I");
GoodsMeasuresForWsResponse.Measures.Measure selectedMeasure = null;
Double selectedDuty = null;
int rank = Integer.MAX_VALUE;
var measures = filterToNewestMeasuresPerType(measForWsResponse.join().getReturn().getResult().getMeasures().getMeasure());
for (var measure : measures) {
var measureType = MeasureType.fromMeasureCode(measure.getMeasureType().getMeasureType());
boolean maybeRelevant = measureType.map(MeasureType::containsRelevantDuty).orElse(false);
if (maybeRelevant) {
var duty = extractDuty(measure);
if (rank > measureType.get().ordinal() && duty.isPresent()) {
rank = measureType.get().ordinal();
selectedDuty = duty.get();
selectedMeasure = measure;
}
}
customMeasures.add(new CustomMeasureDTO(
measure.getMeasureType().getMeasureType(),
measure.getMeasureType().getDescription(),
measure.getRegulationId(),
measure.getDutyRate()));
}
if (selectedDuty != null) {
return new CustomDTO(false, selectedDuty, customMeasures, countryId);
}
} catch (Exception e) {
// just continue
}
return propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.TARIFF_RATE).map(PropertyDTO::getCurrentValue).map(Double::valueOf).map(d -> new CustomDTO(d, customMeasures, countryId)).orElseThrow(() -> new InternalErrorException("Unable to load default custom rate. Please contact support."));
}
public CompletableFuture<CustomDTO> getTariffRate(String hsCode, Integer countryId) {
return CompletableFuture.completedFuture(getTariffRateImmediate(hsCode, countryId));
}
public List<TaxationResolverResponse> getTariffRates(List<TaxationResolverRequest> requests) {
Map<TaxationResolverRequest, List<GoodsMeasForWsResponse>> goodMeasures = doRequests(requests);
return goodMeasures.keySet().stream().map(r -> mapToResponse(r, goodMeasures.get(r))).toList();
}
private TaxationResolverResponse mapToResponse(TaxationResolverRequest request, List<GoodsMeasForWsResponse> measForWsResponse) {
try {
String selectedHsCode = null;
Double selectedDuty = null;
String selectedMeasure = null;
double maxDuty = Double.MIN_VALUE;
double minDuty = Double.MAX_VALUE;
int rank = Integer.MAX_VALUE;
var resp = measForWsResponse.stream().collect(
Collectors.toMap(
r -> r,
r -> filterToNewestMeasuresPerType(r.getReturn().getResult().getMeasures().getMeasure())
));
for (var entry : resp.entrySet()) {
for (var measure : entry.getValue()) {
var measureType = MeasureType.fromMeasureCode(measure.getMeasureType().getMeasureType());
boolean maybeRelevant = measureType.map(MeasureType::containsRelevantDuty).orElse(false);
if (maybeRelevant) {
var duty = extractDuty(measure);
if (duty.isPresent()) {
maxDuty = Math.max(maxDuty, duty.get());
minDuty = Math.min(minDuty, duty.get());
if (rank > measureType.get().ordinal()) {
rank = measureType.get().ordinal();
selectedDuty = duty.get();
selectedMeasure = measureType.map(MeasureType::getMeasureCode).orElse(null);
selectedHsCode = entry.getKey().getReturn().getResult().getRequest().getGoodsCode();
}
}
}
}
}
if (selectedDuty != null && (maxDuty - minDuty < 2)) {
return new TaxationResolverResponse(selectedDuty, selectedMeasure, selectedHsCode, request.material(), request.countryId());
}
} catch (Exception e) {
// just continue
}
return new TaxationResolverResponse(null, null, null, request.material(), request.countryId());
}
public record TaxationResolverSingleRequest(String hsCode, Integer countryId, TaxationResolverRequest origin) {
}
public record TaxationResolverRequest(Material material, Integer countryId) {
}
public record TaxationResolverResponse(@Nullable Double tariffRate,
@Nullable String measureCode,
@Nullable String actualHsCode,
Material material,
Integer countryId) {
}
}

View file

@ -0,0 +1,58 @@
package de.avatic.lcc.service.api;
import de.avatic.lcc.model.zolltarifnummern.ZolltarifnummernResponse;
import de.avatic.lcc.model.zolltarifnummern.ZolltarifnummernResponseEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Service
public class ZolltarifnummernApiService {
private static final Logger logger = LoggerFactory.getLogger(ZolltarifnummernApiService.class);
private static final String API_V_1 = "https://www.zolltarifnummern.de/api/v1/cnSuggest";
private final RestTemplate restTemplate;
public ZolltarifnummernApiService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Async("customLookupExecutor")
public CompletableFuture<Set<String>> getDeclarableHsCodes(String incompleteHsCode) {
try {
String url = UriComponentsBuilder.fromUriString(API_V_1)
.queryParam("term", incompleteHsCode)
.queryParam("lang", "en")
.encode()
.toUriString();
var resp = restTemplate.getForObject(url, ZolltarifnummernResponse.class);
if (resp != null && resp.getSuggestions() != null) {
return CompletableFuture.completedFuture(resp.getSuggestions().stream()
.map(ZolltarifnummernResponseEntry::getCode)
.filter(s -> s.startsWith(incompleteHsCode))
.filter(s -> s.length() >= 10).collect(Collectors.toSet()));
}
} catch (Throwable t) {
logger.error("HS code lookup failed with exception \"{}\"", t.getMessage());
// just continue
}
logger.warn("Unable to load tarif numbers for HS code {}", incompleteHsCode);
return CompletableFuture.completedFuture(Collections.emptySet());
}
}

View file

@ -1,117 +0,0 @@
package de.avatic.lcc.service.calculation;
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
import de.avatic.lcc.dto.calculation.edit.SetDataDTO;
import de.avatic.lcc.model.db.packaging.PackagingDimension;
import de.avatic.lcc.model.db.premises.Premise;
import de.avatic.lcc.model.db.properties.PackagingProperty;
import de.avatic.lcc.model.db.properties.PackagingPropertyMappingId;
import de.avatic.lcc.repositories.MaterialRepository;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.packaging.PackagingDimensionRepository;
import de.avatic.lcc.repositories.packaging.PackagingPropertiesRepository;
import de.avatic.lcc.repositories.packaging.PackagingRepository;
import de.avatic.lcc.repositories.premise.PremiseRepository;
import de.avatic.lcc.repositories.users.UserNodeRepository;
import de.avatic.lcc.service.api.CustomApiService;
import de.avatic.lcc.service.access.PremisesService;
import de.avatic.lcc.service.api.EUTaxationApiWrapperService;
import de.avatic.lcc.service.users.AuthorizationService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
@Service
public class ChangeMaterialService {
private final PremiseRepository premiseRepository;
private final PremisesService premisesService;
private final CustomApiService customApiService;
private final NodeRepository nodeRepository;
private final UserNodeRepository userNodeRepository;
private final PackagingRepository packagingRepository;
private final PackagingDimensionRepository packagingDimensionRepository;
private final PackagingPropertiesRepository packagingPropertiesRepository;
private final MaterialRepository materialRepository;
private final AuthorizationService authorizationService;
private final EUTaxationApiWrapperService eUTaxationApiWrapperService;
public ChangeMaterialService(PremiseRepository premiseRepository, PremisesService premisesService, CustomApiService customApiService, NodeRepository nodeRepository, UserNodeRepository userNodeRepository, PackagingRepository packagingRepository, PackagingDimensionRepository packagingDimensionRepository, PackagingPropertiesRepository packagingPropertiesRepository, MaterialRepository materialRepository, AuthorizationService authorizationService, EUTaxationApiWrapperService eUTaxationApiWrapperService) {
this.premiseRepository = premiseRepository;
this.premisesService = premisesService;
this.customApiService = customApiService;
this.nodeRepository = nodeRepository;
this.userNodeRepository = userNodeRepository;
this.packagingRepository = packagingRepository;
this.packagingDimensionRepository = packagingDimensionRepository;
this.packagingPropertiesRepository = packagingPropertiesRepository;
this.materialRepository = materialRepository;
this.authorizationService = authorizationService;
this.eUTaxationApiWrapperService = eUTaxationApiWrapperService;
}
@Transactional
public List<PremiseDetailDTO> setMaterial(SetDataDTO dto) {
var userId = authorizationService.getUserId();
Integer materialId = dto.getMaterialId();
List<Integer> premiseIds = dto.getPremiseId();
if (materialId == null || premiseIds == null || premiseIds.isEmpty())
throw new IllegalArgumentException("No supplier supplierNodeId or premises given");
// get all premises first.
List<Premise> allPremises = premiseRepository.getPremisesById(premiseIds);
// find resulting duplicates, split into "keep" and "to be deleted".
Map<Integer, Premise> uniqueMap = new HashMap<>();
List<Premise> premisesToBeDeleted = new ArrayList<>();
allPremises.forEach(p -> {
if (null != uniqueMap.putIfAbsent(p.getSupplierNodeId(), p)) premisesToBeDeleted.add(p);
});
Collection<Premise> premisesToProcess = uniqueMap.values();
// check if user owns all premises:
if (allPremises.stream().anyMatch(p -> !p.getUserId().equals(userId)))
throw new IllegalArgumentException("Not authorized to change material of premises owned by other users");
// check for any other collisions, and mark as "to be deleted":
premisesToBeDeleted.addAll(premisesToProcess.stream().map(p -> premiseRepository.getCollidingPremisesOnChange(userId, p.getId(), materialId, p.getSupplierNodeId())).flatMap(List::stream).toList());
if(dto.isUpdateMasterData()) {
var material = materialRepository.getById(materialId).orElseThrow(() -> new IllegalArgumentException("No material with id " + materialId));
for (var premise : premisesToProcess) {
var countryId = dto.isUserSupplierNode() ? userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow().getCountryId() : nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow().getCountryId();
var tariffRate = eUTaxationApiWrapperService.getTariffRateImmediate(material.getHsCode(), countryId);
premiseRepository.updateMaterial(Collections.singletonList(premise.getId()), material.getHsCode(), BigDecimal.valueOf(tariffRate.getValue()));
if (!dto.isUserSupplierNode()) {
var packaging = packagingRepository.getByMaterialIdAndSupplierId(dto.getMaterialId(), premise.getSupplierNodeId());
Optional<PackagingDimension> dimension = packaging.flatMap(p -> packagingDimensionRepository.getById(p.getHuId()));
if (dimension.isPresent()) {
boolean stackable = packagingPropertiesRepository.getByPackagingIdAndType(packaging.get().getId(), PackagingPropertyMappingId.STACKABLE.name()).map(PackagingProperty::getValue).map(Boolean::valueOf).orElse(false);
boolean mixable = packagingPropertiesRepository.getByPackagingIdAndType(packaging.get().getId(), PackagingPropertyMappingId.MIXABLE.name()).map(PackagingProperty::getValue).map(Boolean::valueOf).orElse(false);
premiseRepository.updatePackaging(Collections.singletonList(premise.getId()), dimension.get(), stackable, mixable);
}
}
}
}
// actually update materialId.
premiseRepository.setMaterialId(premisesToProcess.stream().map(Premise::getId).toList(), materialId);
//delete all conflicting premises:
premisesService.delete(premisesToBeDeleted.stream().map(Premise::getId).toList());
return premisesService.getPremises(premisesToProcess.stream().map(Premise::getId).toList());
}
}

View file

@ -1,147 +0,0 @@
package de.avatic.lcc.service.calculation;
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
import de.avatic.lcc.dto.calculation.edit.SetDataDTO;
import de.avatic.lcc.model.db.nodes.Node;
import de.avatic.lcc.model.db.packaging.PackagingDimension;
import de.avatic.lcc.model.db.premises.Premise;
import de.avatic.lcc.model.db.premises.route.Destination;
import de.avatic.lcc.model.db.properties.PackagingProperty;
import de.avatic.lcc.model.db.properties.PackagingPropertyMappingId;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.packaging.PackagingDimensionRepository;
import de.avatic.lcc.repositories.packaging.PackagingPropertiesRepository;
import de.avatic.lcc.repositories.packaging.PackagingRepository;
import de.avatic.lcc.repositories.premise.*;
import de.avatic.lcc.repositories.users.UserNodeRepository;
import de.avatic.lcc.service.api.CustomApiService;
import de.avatic.lcc.service.access.DestinationService;
import de.avatic.lcc.service.access.PremisesService;
import de.avatic.lcc.service.api.EUTaxationApiWrapperService;
import de.avatic.lcc.service.users.AuthorizationService;
import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Service
public class ChangeSupplierService {
private final PremiseRepository premiseRepository;
private final DestinationService destinationService;
private final RoutingService routingService;
private final PremisesService premisesService;
private final CustomApiService customApiService;
private final NodeRepository nodeRepository;
private final UserNodeRepository userNodeRepository;
private final PackagingRepository packagingRepository;
private final PackagingDimensionRepository packagingDimensionRepository;
private final PackagingPropertiesRepository packagingPropertiesRepository;
private final DestinationRepository destinationRepository;
private final RouteRepository routeRepository;
private final RouteSectionRepository routeSectionRepository;
private final RouteNodeRepository routeNodeRepository;
private final AuthorizationService authorizationService;
private final EUTaxationApiWrapperService eUTaxationApiWrapperService;
public ChangeSupplierService(PremiseRepository premiseRepository, DestinationService destinationService, RoutingService routingService, PremisesService premisesService, CustomApiService customApiService, NodeRepository nodeRepository, UserNodeRepository userNodeRepository, PackagingRepository packagingRepository, PackagingDimensionRepository packagingDimensionRepository, PackagingPropertiesRepository packagingPropertiesRepository, DestinationRepository destinationRepository, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, AuthorizationService authorizationService, EUTaxationApiWrapperService eUTaxationApiWrapperService) {
this.premiseRepository = premiseRepository;
this.destinationService = destinationService;
this.routingService = routingService;
this.premisesService = premisesService;
this.customApiService = customApiService;
this.nodeRepository = nodeRepository;
this.userNodeRepository = userNodeRepository;
this.packagingRepository = packagingRepository;
this.packagingDimensionRepository = packagingDimensionRepository;
this.packagingPropertiesRepository = packagingPropertiesRepository;
this.destinationRepository = destinationRepository;
this.routeRepository = routeRepository;
this.routeSectionRepository = routeSectionRepository;
this.routeNodeRepository = routeNodeRepository;
this.authorizationService = authorizationService;
this.eUTaxationApiWrapperService = eUTaxationApiWrapperService;
}
@Transactional
public List<PremiseDetailDTO> setSupplier(SetDataDTO dto) {
var userId = authorizationService.getUserId();
Integer supplierNodeId = dto.getSupplierNodeId();
List<Integer> premiseIds = dto.getPremiseId();
if (supplierNodeId == null || premiseIds == null || premiseIds.isEmpty())
throw new InvalidArgumentException("No supplier supplierNodeId or premises given");
Node supplier = dto.isUserSupplierNode() ? userNodeRepository.getById(supplierNodeId).orElseThrow() : nodeRepository.getById(supplierNodeId).orElseThrow();
// get all premises first.
List<Premise> allPremises = premiseRepository.getPremisesById(premiseIds);
// find resulting duplicates, split into "keep" and "to be deleted".
Map<Integer, Premise> uniqueMap = new HashMap<>();
List<Premise> premisesToBeDeleted = new ArrayList<>();
allPremises.forEach(p -> {
if (null != uniqueMap.putIfAbsent(p.getMaterialId(), p)) premisesToBeDeleted.add(p);
});
Collection<Premise> premisesToProcess = uniqueMap.values();
// check if user owns all premises:
if (allPremises.stream().anyMatch(p -> !p.getUserId().equals(userId)))
throw new IllegalArgumentException("Not authorized to change suppliers of premises owned by other users");
// check for any other collisions, and mark as "to be deleted":
premisesToBeDeleted.addAll(premisesToProcess.stream().map(p -> premiseRepository.getCollidingPremisesOnChange(userId, p.getId(), p.getMaterialId(), supplierNodeId)).flatMap(List::stream).toList());
// delete all routes of all destinations
destinationService.deleteAllDestinationsByPremiseId(premisesToProcess.stream().map(Premise::getId).toList(), true);
//recalculate routes:
for (Premise premise : premisesToProcess) {
List<Destination> destination = destinationRepository.getByPremiseId(premise.getId());
for (Destination d : destination) {
Node destinationNode = nodeRepository.getById(d.getDestinationNodeId()).orElseThrow();
destinationService.findRouteAndSave(d.getId(), destinationNode, supplier, dto.isUserSupplierNode() );
}
}
//update master data:
if (dto.isUpdateMasterData()) {
for (var premise : premisesToProcess) {
var tariffRate = eUTaxationApiWrapperService.getTariffRateImmediate(premise.getHsCode(), supplier.getCountryId());
premiseRepository.updateTariffRate(premise.getId(), tariffRate.getValue());
if (!dto.isUserSupplierNode()) {
var packaging = packagingRepository.getByMaterialIdAndSupplierId(premise.getMaterialId(), supplierNodeId);
Optional<PackagingDimension> dimension = packaging.flatMap(p -> packagingDimensionRepository.getById(p.getHuId()));
if (dimension.isPresent()) {
boolean stackable = packagingPropertiesRepository.getByPackagingIdAndType(packaging.get().getId(), PackagingPropertyMappingId.STACKABLE.name()).map(PackagingProperty::getValue).map(Boolean::valueOf).orElse(false);
boolean mixable = packagingPropertiesRepository.getByPackagingIdAndType(packaging.get().getId(), PackagingPropertyMappingId.MIXABLE.name()).map(PackagingProperty::getValue).map(Boolean::valueOf).orElse(false);
premiseRepository.updatePackaging(Collections.singletonList(premise.getId()), dimension.get(), stackable, mixable);
}
}
}
}
// actually update supplier supplierNodeId.
premiseRepository.setSupplierId(premisesToProcess.stream().map(Premise::getId).toList(), supplier, dto.isUserSupplierNode());
//delete all conflicting premises:
if(!premisesToBeDeleted.isEmpty())
premisesService.delete(premisesToBeDeleted.stream().map(Premise::getId).toList());
return premisesService.getPremises(premisesToProcess.stream().map(Premise::getId).toList());
}
}

View file

@ -1,21 +0,0 @@
package de.avatic.lcc.service.calculation;
import de.avatic.lcc.repositories.NomenclatureRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class NomenclatureService {
private final NomenclatureRepository nomenclatureRepository;
public NomenclatureService(NomenclatureRepository nomenclatureRepository) {
this.nomenclatureRepository = nomenclatureRepository;
}
public List<String> getNomenclature(String hsCode) {
return nomenclatureRepository.searchHsCode(hsCode);
}
}

View file

@ -15,7 +15,7 @@ import de.avatic.lcc.repositories.premise.PremiseRepository;
import de.avatic.lcc.repositories.users.UserNodeRepository; import de.avatic.lcc.repositories.users.UserNodeRepository;
import de.avatic.lcc.service.api.CustomApiService; import de.avatic.lcc.service.api.CustomApiService;
import de.avatic.lcc.service.access.DestinationService; import de.avatic.lcc.service.access.DestinationService;
import de.avatic.lcc.service.api.EUTaxationApiWrapperService; import de.avatic.lcc.service.api.TaxationResolverService;
import de.avatic.lcc.service.transformer.generic.DimensionTransformer; import de.avatic.lcc.service.transformer.generic.DimensionTransformer;
import de.avatic.lcc.service.transformer.premise.PremiseTransformer; import de.avatic.lcc.service.transformer.premise.PremiseTransformer;
import de.avatic.lcc.service.users.AuthorizationService; import de.avatic.lcc.service.users.AuthorizationService;
@ -41,11 +41,10 @@ public class PremiseCreationService {
private final PackagingRepository packagingRepository; private final PackagingRepository packagingRepository;
private final PackagingDimensionRepository packagingDimensionRepository; private final PackagingDimensionRepository packagingDimensionRepository;
private final PackagingPropertiesRepository packagingPropertiesRepository; private final PackagingPropertiesRepository packagingPropertiesRepository;
private final CustomApiService customApiService;
private final AuthorizationService authorizationService; private final AuthorizationService authorizationService;
private final EUTaxationApiWrapperService eUTaxationApiWrapperService; private final TaxationResolverService taxationResolverService;
public PremiseCreationService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DestinationService destinationService, UserNodeRepository userNodeRepository, NodeRepository nodeRepository, MaterialRepository materialRepository, DimensionTransformer dimensionTransformer, PackagingRepository packagingRepository, PackagingDimensionRepository packagingDimensionRepository, PackagingPropertiesRepository packagingPropertiesRepository, CustomApiService customApiService, AuthorizationService authorizationService, EUTaxationApiWrapperService eUTaxationApiWrapperService) { public PremiseCreationService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DestinationService destinationService, UserNodeRepository userNodeRepository, NodeRepository nodeRepository, MaterialRepository materialRepository, DimensionTransformer dimensionTransformer, PackagingRepository packagingRepository, PackagingDimensionRepository packagingDimensionRepository, PackagingPropertiesRepository packagingPropertiesRepository, CustomApiService customApiService, AuthorizationService authorizationService, TaxationResolverService taxationResolverService) {
this.premiseRepository = premiseRepository; this.premiseRepository = premiseRepository;
this.premiseTransformer = premiseTransformer; this.premiseTransformer = premiseTransformer;
this.destinationService = destinationService; this.destinationService = destinationService;
@ -56,9 +55,9 @@ public class PremiseCreationService {
this.packagingRepository = packagingRepository; this.packagingRepository = packagingRepository;
this.packagingDimensionRepository = packagingDimensionRepository; this.packagingDimensionRepository = packagingDimensionRepository;
this.packagingPropertiesRepository = packagingPropertiesRepository; this.packagingPropertiesRepository = packagingPropertiesRepository;
this.customApiService = customApiService;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.eUTaxationApiWrapperService = eUTaxationApiWrapperService; this.taxationResolverService = taxationResolverService;
} }
@Transactional @Transactional
@ -76,17 +75,19 @@ public class PremiseCreationService {
premises.forEach(p -> verifyNode(p, userId)); premises.forEach(p -> verifyNode(p, userId));
verifyMaterial(materialIds); verifyMaterial(materialIds);
List<TaxationResolverService.TaxationResolverResponse> tariffs = taxationResolverService.getTariffRates(premises.stream().map(p -> new TaxationResolverService.TaxationResolverRequest(materialRepository.getById(p.getMaterialId()).orElseThrow(), p.getCountryId())).distinct().toList());
premises.forEach(p -> { premises.forEach(p -> {
if (p.getPremise() == null) { // create new if (p.getPremise() == null) { // create new
p.setId(premiseRepository.insert(p.getMaterialId(), p.getSupplierId(), p.getUserSupplierId(), p.getGeoLat(), p.getGeoLng(), p.getCountryId(), userId)); p.setId(premiseRepository.insert(p.getMaterialId(), p.getSupplierId(), p.getUserSupplierId(), p.getGeoLat(), p.getGeoLng(), p.getCountryId(), userId));
fillPremise(p, userId); fillPremise(p, tariffs, userId);
} else if (p.getPremise().getState().equals(PremiseState.DRAFT)) { // recycle } else if (p.getPremise().getState().equals(PremiseState.DRAFT)) { // recycle
p.setId(p.getPremise().getId()); p.setId(p.getPremise().getId());
if (createEmpty) { if (createEmpty) {
// reset to defaults. // reset to defaults.
fillPremise(p, userId); fillPremise(p, tariffs, userId);
} }
} else if (p.getPremise().getState().equals(PremiseState.COMPLETED)) { } else if (p.getPremise().getState().equals(PremiseState.COMPLETED)) {
@ -105,13 +106,13 @@ public class PremiseCreationService {
private void copyPremise(TemporaryPremise p, Integer userId) { private void copyPremise(TemporaryPremise p, Integer userId) {
var old = p.getPremise(); var old = p.getPremise();
premiseRepository.updateMaterial(Collections.singletonList(p.getId()), old.getHsCode(), old.getTariffRate()); premiseRepository.updateMaterial(Collections.singletonList(p.getId()), old.getHsCode(), old.getTariffRate(), old.getTariffUnlocked());
premiseRepository.updatePrice(Collections.singletonList(p.getId()), old.getMaterialCost(), old.getFcaEnabled(), old.getOverseaShare()); premiseRepository.updatePrice(Collections.singletonList(p.getId()), old.getMaterialCost(), old.getFcaEnabled(), old.getOverseaShare());
premiseRepository.updatePackaging(Collections.singletonList(p.getId()), dimensionTransformer.toDimensionEntity(old), old.getHuStackable(), old.getHuMixable()); premiseRepository.updatePackaging(Collections.singletonList(p.getId()), dimensionTransformer.toDimensionEntity(old), old.getHuStackable(), old.getHuMixable());
premiseRepository.setPackagingId(p.getId(), old.getPackagingId()); premiseRepository.setPackagingId(p.getId(), old.getPackagingId());
} }
private void fillPremise(TemporaryPremise p, Integer userId) { private void fillPremise(TemporaryPremise p, List<TaxationResolverService.TaxationResolverResponse> tariffs, Integer userId) {
if (!p.isUserSupplier()) { if (!p.isUserSupplier()) {
var packaging = packagingRepository.getByMaterialIdAndSupplierId(p.getMaterialId(), p.getSupplierId()); var packaging = packagingRepository.getByMaterialIdAndSupplierId(p.getMaterialId(), p.getSupplierId());
@ -126,18 +127,13 @@ public class PremiseCreationService {
} }
} }
var material = materialRepository.getById(p.getMaterialId()); tariffs.stream()
material.ifPresent(value -> premiseRepository.updateMaterial(Collections.singletonList(p.getId()), value.getHsCode(), BigDecimal.valueOf(eUTaxationApiWrapperService.getTariffRateImmediate(value.getHsCode(), getCountryId(p)).getValue()))); .filter(r -> r.material().getId().equals(p.getMaterialId()))
.findFirst()
} .ifPresent(value -> premiseRepository.updateMaterial(Collections.singletonList(
p.getId()),
private Integer getCountryId(TemporaryPremise p) { value.actualHsCode(),
value.tariffRate() == null ? null : BigDecimal.valueOf(value.tariffRate())));
if (p.isUserSupplier()) {
return userNodeRepository.getById(p.getUserSupplierId()).orElseThrow().getCountryId();
} else {
return nodeRepository.getById(p.getSupplierId()).orElseThrow().getCountryId();
}
} }
private void findExistingPremise(TemporaryPremise premise, boolean createEmpty, Integer userId) { private void findExistingPremise(TemporaryPremise premise, boolean createEmpty, Integer userId) {

View file

@ -21,7 +21,7 @@ import de.avatic.lcc.repositories.rates.MatrixRateRepository;
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository; import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
import de.avatic.lcc.service.access.PropertyService; import de.avatic.lcc.service.access.PropertyService;
import de.avatic.lcc.service.api.CustomApiService; import de.avatic.lcc.service.api.CustomApiService;
import de.avatic.lcc.service.api.EUTaxationApiWrapperService; import de.avatic.lcc.service.api.TaxationResolverService;
import de.avatic.lcc.service.transformer.generic.DimensionTransformer; import de.avatic.lcc.service.transformer.generic.DimensionTransformer;
import de.avatic.lcc.util.exception.internalerror.PremiseValidationError; import de.avatic.lcc.util.exception.internalerror.PremiseValidationError;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
@ -30,11 +30,9 @@ import org.springframework.stereotype.Service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalUnit;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@Service @Service
public class PreCalculationCheckService { public class PreCalculationCheckService {
@ -56,9 +54,9 @@ public class PreCalculationCheckService {
private final ValidityPeriodRepository validityPeriodRepository; private final ValidityPeriodRepository validityPeriodRepository;
private final PropertySetRepository propertySetRepository; private final PropertySetRepository propertySetRepository;
private final PropertyRepository propertyRepository; private final PropertyRepository propertyRepository;
private final EUTaxationApiWrapperService eUTaxationApiWrapperService; private final TaxationResolverService eUTaxationResolverService;
public PreCalculationCheckService(PremiseRepository premiseRepository, CustomApiService customApiService, DestinationRepository destinationRepository, RouteRepository routeRepository, NodeRepository nodeRepository, DimensionTransformer dimensionTransformer, PropertyService propertyService, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, MatrixRateRepository matrixRateRepository, ContainerRateRepository containerRateRepository, ValidityPeriodRepository validityPeriodRepository, PropertySetRepository propertySetRepository, PropertyRepository propertyRepository, EUTaxationApiWrapperService eUTaxationApiWrapperService) { public PreCalculationCheckService(PremiseRepository premiseRepository, CustomApiService customApiService, DestinationRepository destinationRepository, RouteRepository routeRepository, NodeRepository nodeRepository, DimensionTransformer dimensionTransformer, PropertyService propertyService, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, MatrixRateRepository matrixRateRepository, ContainerRateRepository containerRateRepository, ValidityPeriodRepository validityPeriodRepository, PropertySetRepository propertySetRepository, PropertyRepository propertyRepository, TaxationResolverService eUTaxationResolverService) {
this.premiseRepository = premiseRepository; this.premiseRepository = premiseRepository;
this.customApiService = customApiService; this.customApiService = customApiService;
this.destinationRepository = destinationRepository; this.destinationRepository = destinationRepository;
@ -74,7 +72,7 @@ public class PreCalculationCheckService {
this.validityPeriodRepository = validityPeriodRepository; this.validityPeriodRepository = validityPeriodRepository;
this.propertySetRepository = propertySetRepository; this.propertySetRepository = propertySetRepository;
this.propertyRepository = propertyRepository; this.propertyRepository = propertyRepository;
this.eUTaxationApiWrapperService = eUTaxationApiWrapperService; this.eUTaxationResolverService = eUTaxationResolverService;
} }
@Async("calculationExecutor") @Async("calculationExecutor")
@ -327,7 +325,7 @@ public class PreCalculationCheckService {
private void materialCheck(Premise premise) { private void materialCheck(Premise premise) {
var isDeclarable = eUTaxationApiWrapperService.validate(premise.getHsCode()); var isDeclarable = eUTaxationResolverService.validate(premise.getHsCode());
if (!isDeclarable) if (!isDeclarable)
throw new PremiseValidationError("Invalid HS code."); throw new PremiseValidationError("Invalid HS code.");

View file

@ -110,6 +110,7 @@ public class PremiseTransformer {
dto.setHsCode(entity.getHsCode()); dto.setHsCode(entity.getHsCode());
dto.setTariffRate(entity.getTariffRate() == null ? null : entity.getTariffRate().doubleValue()); dto.setTariffRate(entity.getTariffRate() == null ? null : entity.getTariffRate().doubleValue());
dto.setTariffUnlocked(entity.getTariffUnlocked());
dto.setFcaEnabled(entity.getFcaEnabled()); dto.setFcaEnabled(entity.getFcaEnabled());
dto.setOverseaShare(entity.getOverseaShare() == null ? null : entity.getOverseaShare().doubleValue()); dto.setOverseaShare(entity.getOverseaShare() == null ? null : entity.getOverseaShare().doubleValue());

File diff suppressed because it is too large Load diff

View file

@ -213,21 +213,35 @@ CREATE TABLE IF NOT EXISTS outbound_country_mapping
CREATE TABLE IF NOT EXISTS distance_matrix CREATE TABLE IF NOT EXISTS distance_matrix
( (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
from_node_id INT NOT NULL, from_node_id INT DEFAULT NULL,
to_node_id INT NOT NULL, to_node_id INT DEFAULT NULL,
from_geo_lat DECIMAL(8, 4) CHECK (from_geo_lat BETWEEN -90 AND 90), from_user_node_id INT DEFAULT NULL,
from_geo_lng DECIMAL(8, 4) CHECK (from_geo_lng BETWEEN -180 AND 180), to_user_node_id INT DEFAULT NULL,
to_geo_lat DECIMAL(8, 4) CHECK (to_geo_lat BETWEEN -90 AND 90), from_geo_lat DECIMAL(8, 4) CHECK (from_geo_lat BETWEEN -90 AND 90),
to_geo_lng DECIMAL(8, 4) CHECK (to_geo_lng BETWEEN -180 AND 180), from_geo_lng DECIMAL(8, 4) CHECK (from_geo_lng BETWEEN -180 AND 180),
distance DECIMAL(15, 2) NOT NULL COMMENT 'travel distance between the two nodes in meters', to_geo_lat DECIMAL(8, 4) CHECK (to_geo_lat BETWEEN -90 AND 90),
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, to_geo_lng DECIMAL(8, 4) CHECK (to_geo_lng BETWEEN -180 AND 180),
state CHAR(10) NOT NULL, distance DECIMAL(15, 2) NOT NULL COMMENT 'travel distance between the two nodes in meters',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
state CHAR(10) NOT NULL,
FOREIGN KEY (from_node_id) REFERENCES node (id), FOREIGN KEY (from_node_id) REFERENCES node (id),
FOREIGN KEY (to_node_id) REFERENCES node (id), FOREIGN KEY (to_node_id) REFERENCES node (id),
FOREIGN KEY (from_user_node_id) REFERENCES sys_user_node (id),
FOREIGN KEY (to_user_node_id) REFERENCES sys_user_node (id),
CONSTRAINT `chk_distance_matrix_state` CHECK (`state` IN CONSTRAINT `chk_distance_matrix_state` CHECK (`state` IN
('VALID', 'STALE')), ('VALID', 'STALE')),
INDEX idx_from_to_nodes (from_node_id, to_node_id) CONSTRAINT `chk_from_node_xor` CHECK (
(from_node_id IS NOT NULL AND from_user_node_id IS NULL) OR
(from_node_id IS NULL AND from_user_node_id IS NOT NULL)
),
CONSTRAINT `chk_to_node_xor` CHECK (
(to_node_id IS NOT NULL AND to_user_node_id IS NULL) OR
(to_node_id IS NULL AND to_user_node_id IS NOT NULL)
),
INDEX idx_from_to_nodes (from_node_id, to_node_id),
INDEX idx_user_from_to_nodes (from_user_node_id, to_user_node_id),
CONSTRAINT uk_nodes_unique UNIQUE (from_node_id, to_node_id, from_user_node_id, to_user_node_id)
); );
-- container rates -- container rates
@ -375,7 +389,9 @@ CREATE TABLE IF NOT EXISTS premise
is_fca_enabled BOOLEAN DEFAULT FALSE, is_fca_enabled BOOLEAN DEFAULT FALSE,
oversea_share DECIMAL(8, 4) DEFAULT NULL, oversea_share DECIMAL(8, 4) DEFAULT NULL,
hs_code CHAR(11) DEFAULT NULL, hs_code CHAR(11) DEFAULT NULL,
tariff_measure INT UNSIGNED DEFAULT NULL COMMENT 'measure code of the selected tariff',
tariff_rate DECIMAL(8, 4) DEFAULT NULL, tariff_rate DECIMAL(8, 4) DEFAULT NULL,
tariff_unlocked BOOLEAN DEFAULT FALSE,
state CHAR(10) NOT NULL DEFAULT 'DRAFT', state CHAR(10) NOT NULL DEFAULT 'DRAFT',
individual_hu_length INT UNSIGNED COMMENT 'user entered dimensions in mm (if system-wide packaging is used, packaging dimensions are copied here after creation)', individual_hu_length INT UNSIGNED COMMENT 'user entered dimensions in mm (if system-wide packaging is used, packaging dimensions are copied here after creation)',
individual_hu_height INT UNSIGNED COMMENT 'user entered dimensions in mm (if system-wide packaging is used, packaging dimensions are copied here after creation)', individual_hu_height INT UNSIGNED COMMENT 'user entered dimensions in mm (if system-wide packaging is used, packaging dimensions are copied here after creation)',

View file

@ -59,13 +59,6 @@ CREATE TABLE IF NOT EXISTS `country`
CHECK (`region_code` IN ('EMEA', 'LATAM', 'APAC', 'NAM')) CHECK (`region_code` IN ('EMEA', 'LATAM', 'APAC', 'NAM'))
) COMMENT 'Master data table for country information and regional classification'; ) COMMENT 'Master data table for country information and regional classification';
CREATE TABLE IF NOT EXISTS `nomenclature`
(
`hs_code` VARCHAR(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
UNIQUE KEY `uk_nomenclature_name` (`hs_code`)
) COMMENT 'Master data table for nomenclature information';
CREATE TABLE IF NOT EXISTS `country_property_type` CREATE TABLE IF NOT EXISTS `country_property_type`
( (
`id` INT NOT NULL AUTO_INCREMENT, `id` INT NOT NULL AUTO_INCREMENT,
@ -220,21 +213,35 @@ CREATE TABLE IF NOT EXISTS outbound_country_mapping
CREATE TABLE IF NOT EXISTS distance_matrix CREATE TABLE IF NOT EXISTS distance_matrix
( (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
from_node_id INT NOT NULL, from_node_id INT DEFAULT NULL,
to_node_id INT NOT NULL, to_node_id INT DEFAULT NULL,
from_geo_lat DECIMAL(8, 4) CHECK (from_geo_lat BETWEEN -90 AND 90), from_user_node_id INT DEFAULT NULL,
from_geo_lng DECIMAL(8, 4) CHECK (from_geo_lng BETWEEN -180 AND 180), to_user_node_id INT DEFAULT NULL,
to_geo_lat DECIMAL(8, 4) CHECK (to_geo_lat BETWEEN -90 AND 90), from_geo_lat DECIMAL(8, 4) CHECK (from_geo_lat BETWEEN -90 AND 90),
to_geo_lng DECIMAL(8, 4) CHECK (to_geo_lng BETWEEN -180 AND 180), from_geo_lng DECIMAL(8, 4) CHECK (from_geo_lng BETWEEN -180 AND 180),
distance DECIMAL(15, 2) NOT NULL COMMENT 'travel distance between the two nodes in meters', to_geo_lat DECIMAL(8, 4) CHECK (to_geo_lat BETWEEN -90 AND 90),
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, to_geo_lng DECIMAL(8, 4) CHECK (to_geo_lng BETWEEN -180 AND 180),
state CHAR(10) NOT NULL, distance DECIMAL(15, 2) NOT NULL COMMENT 'travel distance between the two nodes in meters',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
state CHAR(10) NOT NULL,
FOREIGN KEY (from_node_id) REFERENCES node (id), FOREIGN KEY (from_node_id) REFERENCES node (id),
FOREIGN KEY (to_node_id) REFERENCES node (id), FOREIGN KEY (to_node_id) REFERENCES node (id),
FOREIGN KEY (from_user_node_id) REFERENCES sys_user_node (id),
FOREIGN KEY (to_user_node_id) REFERENCES sys_user_node (id),
CONSTRAINT `chk_distance_matrix_state` CHECK (`state` IN CONSTRAINT `chk_distance_matrix_state` CHECK (`state` IN
('VALID', 'STALE')), ('VALID', 'STALE')),
INDEX idx_from_to_nodes (from_node_id, to_node_id) CONSTRAINT `chk_from_node_xor` CHECK (
(from_node_id IS NOT NULL AND from_user_node_id IS NULL) OR
(from_node_id IS NULL AND from_user_node_id IS NOT NULL)
),
CONSTRAINT `chk_to_node_xor` CHECK (
(to_node_id IS NOT NULL AND to_user_node_id IS NULL) OR
(to_node_id IS NULL AND to_user_node_id IS NOT NULL)
),
INDEX idx_from_to_nodes (from_node_id, to_node_id),
INDEX idx_user_from_to_nodes (from_user_node_id, to_user_node_id),
CONSTRAINT uk_nodes_unique UNIQUE (from_node_id, to_node_id, from_user_node_id, to_user_node_id)
); );
-- container rates -- container rates
@ -382,7 +389,9 @@ CREATE TABLE IF NOT EXISTS premise
is_fca_enabled BOOLEAN DEFAULT FALSE, is_fca_enabled BOOLEAN DEFAULT FALSE,
oversea_share DECIMAL(8, 4) DEFAULT NULL, oversea_share DECIMAL(8, 4) DEFAULT NULL,
hs_code CHAR(11) DEFAULT NULL, hs_code CHAR(11) DEFAULT NULL,
tariff_measure CHAR(16) DEFAULT NULL COMMENT 'measure code of the selected tariff',
tariff_rate DECIMAL(8, 4) DEFAULT NULL, tariff_rate DECIMAL(8, 4) DEFAULT NULL,
tariff_unlocked BOOLEAN DEFAULT FALSE,
state CHAR(10) NOT NULL DEFAULT 'DRAFT', state CHAR(10) NOT NULL DEFAULT 'DRAFT',
individual_hu_length INT UNSIGNED COMMENT 'user entered dimensions in mm (if system-wide packaging is used, packaging dimensions are copied here after creation)', individual_hu_length INT UNSIGNED COMMENT 'user entered dimensions in mm (if system-wide packaging is used, packaging dimensions are copied here after creation)',
individual_hu_height INT UNSIGNED COMMENT 'user entered dimensions in mm (if system-wide packaging is used, packaging dimensions are copied here after creation)', individual_hu_height INT UNSIGNED COMMENT 'user entered dimensions in mm (if system-wide packaging is used, packaging dimensions are copied here after creation)',