Centralized notification/error management in frontend, more infos in error log, bugfixing in tariff rate services.

This commit is contained in:
Jan 2025-11-16 18:32:28 +01:00
parent 35b0b8584d
commit 5adadb671e
28 changed files with 872 additions and 560 deletions

View file

@ -1,5 +1,5 @@
<template>
<error-notification></error-notification>
<the-notification-system />
<the-header></the-header>
<router-view v-slot="slotProps">
<transition name="route" mode="out-in">
@ -12,10 +12,10 @@
import TheHeader from "@/components/layout/TheHeader.vue";
import ErrorNotification from "@/components/UI/ErrorNotifcation.vue";
import TheNotificationSystem from "@/components/UI/TheNotificationSystem.vue";
export default {
components: {ErrorNotification, TheHeader},
components: {TheNotificationSystem, TheHeader},
}
</script>

View file

@ -1,5 +1,6 @@
import logger from "@/logger.js";
import {useErrorStore} from "@/store/error.js";
import {config} from "@/config";
const getCsrfToken = () => {
const value = `; ${document.cookie}`;
@ -13,44 +14,32 @@ const getCsrfToken = () => {
let sessionRefreshInterval = null;
let lastActivity = Date.now();
const refreshSession = async () => {
try {
// Ein simpler authenticated GET request hält die Session am Leben
await fetch('/api/session/keepalive', {
method: 'GET',
credentials: 'include'
});
console.log('Session refreshed');
await performRequest(null, 'GET', `${config.backendUrl}/error/`, null);
logger.log('Session refreshed');
} catch (e) {
console.error('Session refresh failed', e);
logger.error('Session refresh failed', e);
}
}
const trackActivity = () => {
lastActivity = Date.now();
}
const startSessionRefresh = () => {
['mousedown', 'keypress', 'scroll', 'touchstart'].forEach(event => {
document.addEventListener(event, trackActivity);
});
if (sessionRefreshInterval) {
clearInterval(sessionRefreshInterval);
}
sessionRefreshInterval = setInterval(() => {
sessionRefreshInterval = setInterval(async () => {
const timeSinceActivity = Date.now() - lastActivity;
if (timeSinceActivity < (12 * 60 * 60 * 1000)) {
refreshSession();
if (timeSinceActivity < (10 * 60 * 1000)) {
await refreshSession();
}
}, 10 * 60 * 1000);
}, 2 * 60 * 1000);
}
const stopSessionRefresh = () => {
@ -93,12 +82,17 @@ const performDownload = async (url, toFile, expectResponse = true, expectedExcep
}
};
const request = {url: url, params: params, expectResponse: expectResponse, expectedException: expectedException, type: 'blob'};
const request = {
url: url,
params: params,
expectResponse: expectResponse,
expectedException: expectedException,
type: 'blob'
};
logger.info("Request:", request);
const resp = await executeRequest(null, request);
const downloadUrl = window.URL.createObjectURL(resp.data);
// Create temporary link element and trigger download
@ -147,17 +141,26 @@ function handleErrorResponse(data, requestingStore, request) {
const error = new Error('Internal backend error');
error.errorObj = errorObj;
const errorStore = useErrorStore();
if (request.expectedException === null || (Array.isArray(request.expectResponse) && !request.expectedException.includes(data.error.title)) || (typeof request.expectedException === 'string' && data.error.title !== request.expectedException)) {
logger.error(errorObj, request.expectedException);
const errorStore = useErrorStore();
void errorStore.addError(errorObj, {store: requestingStore, request: request});
} else {
errorStore.addNotification({
icon: 'warning',
message: data.error.message,
title: data.error.title,
variant: 'exception',
});
}
throw error;
}
const executeRequest = async (requestingStore, request) => {
trackActivity();
const response = await fetch(request.url, request.params
).catch(e => {
const error = {
@ -180,7 +183,7 @@ const executeRequest = async (requestingStore, request) => {
}
let data = null;
if (request.expectResponse) {
if (request.expectResponse) { // backend should return something
try {
if (request.type === 'blob' && response.ok) {
data = await response.blob();
@ -207,7 +210,7 @@ const executeRequest = async (requestingStore, request) => {
} else {
if (!response.ok) {
const data = await response.json().catch(e => {
const data = await response.json().catch(_ => {
const error = {
code: "Return code error " + response.status,
message: "Server returned wrong response code",

View file

@ -1,165 +0,0 @@
<template>
<teleport to="body">
<transition
name="error-container"
tag="div"
class="error-notification-container">
<div class="error-notification" v-if="error">
<div>
<ph-warning size="24"></ph-warning>
</div>
<div class="error-message">
<div class="error-message-title">
{{ title }}
</div>
<div class="error-message-content">
{{ message }}
</div>
<div class="error-view-trace" v-if="trace" @click="activateTrace">
View trace
</div>
</div>
<div class="icon-error-notification">
<ph-x size="24" @click="close"></ph-x>
</div>
<modal :z-index="9001" :state="showTrace"><trace-view :error="error" @close="deactivateTrace"></trace-view></modal>
</div>
</transition>
</teleport>
</template>
<script>
import {useErrorStore} from "@/store/error.js";
import {mapStores} from "pinia";
import TraceView from "@/components/layout/TraceView.vue";
import Modal from "@/components/UI/Modal.vue";
export default {
name: "ErrorNotification",
components: {Modal, TraceView},
data() {
return {
showTrace: false,
}
},
computed: {
...mapStores(useErrorStore),
title() {
return this.error?.title || 'Error';
},
message() {
return this.error?.message || 'An unexpected error occurred';
},
trace() {
return !((this.error?.trace ?? null) === null) || !((this.error?.traceCombined ?? null) === null);
},
error() {
return this.errorStore.lastError;
}
},
methods: {
close() {
this.errorStore.clearErrors();
},
activateTrace() {
this.showTrace = true;
},
deactivateTrace() {
this.showTrace = false;
},
}
}
</script>
<style scoped>
.error-message {
display: flex;
flex-direction: column;
align-items: flex-start;
flex: 1;
}
.error-message-title {
font-size: 1.6rem;
font-weight: 500;
}
.error-message-content {
font-size: 1.4rem;
}
.error-view-trace {
font-size: 1.4rem;
cursor: pointer;
text-decoration: underline;
}
.error-notification-container {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translate(-50%, 0);
z-index: 9000;
pointer-events: none;
}
.error-notification {
min-width: 30rem;
max-width: 100rem;
padding: 1.6rem;
border-radius: 0.8rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1.6rem;
background-color: #BC2B72;
color: #ffffff;
font-size: 1.4rem;
cursor: pointer;
pointer-events: auto;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.icon-error-notification {
cursor: pointer;
transition: all 0.1s ease-in-out;
background-color: #BC2B72;
color: #ffffff;
}
.icon-error-notification:hover, .icon-error-notification:active {
cursor: pointer;
transform: scale(1.1);
}
/* Transition animations */
.error-container-enter-active {
transition: all 0.3s ease;
}
.error-container-leave-active {
transition: all 0.3s ease;
}
.error-container-enter-from {
opacity: 0;
transform: translate(-50%, 100%);
}
.error-container-leave-to {
opacity: 0;
transform: translate(-50%, 100%);
}
.error-container-move {
transition: transform 0.3s ease;
}
</style>

View file

@ -40,6 +40,10 @@ export default {
<style scoped>
.flag-container img {
filter: saturate(0.9) brightness(0.95) hue-rotate(-10deg);
}
.flag-container {
display: flex;
align-items: center;

View file

@ -0,0 +1,173 @@
<template>
<teleport to="body">
<toast ref="toast"></toast>
<!-- <transition-->
<!-- name="error-container"-->
<!-- tag="div"-->
<!-- class="error-notification-container">-->
<!-- <div class="error-notification" v-if="error">-->
<!-- <div>-->
<!-- <ph-warning size="24"></ph-warning>-->
<!-- </div>-->
<!-- <div class="error-message">-->
<!-- <div class="error-message-title">-->
<!-- {{ title }}-->
<!-- </div>-->
<!-- <div class="error-message-content">-->
<!-- {{ message }}-->
<!-- </div>-->
<!-- <div class="error-view-trace" v-if="trace" @click="activateTrace">-->
<!-- View trace-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="icon-error-notification">-->
<!-- <ph-x size="24" @click="close"></ph-x>-->
<!-- </div>-->
<!-- <modal :z-index="9001" :state="showTrace"><trace-view :error="error" @close="deactivateTrace"></trace-view></modal>-->
<!-- </div>-->
<!-- </transition>-->
</teleport>
</template>
<script>
import {useErrorStore} from "@/store/error.js";
import {mapStores} from "pinia";
import Toast from "@/components/UI/Toast.vue";
import logger from "@/logger.js";
export default {
name: "TheNotificationSystem",
components: {Toast},
data() {
return {}
},
computed: {
...mapStores(useErrorStore),
notifications() {
return this.errorStore.notifications;
},
hasNotifications() {
return this.errorStore.notifications?.length !== 0;
}
},
watch: {
hasNotifications(newValue) {
if (newValue)
this.fireToasts();
}
},
methods: {
fireToasts() {
while (this.hasNotifications) {
const msg = this.errorStore.popNotification();
logger.log("fire msg", msg);
if (msg)
this.$refs.toast.addToast({
icon: msg.icon ?? null,
message: msg.message ?? null,
title: msg.title ?? null,
variant: msg.variant ?? 'exception',
duration: msg.duration ?? 8000
});
}
}
}
}
</script>
<style scoped>
.error-message {
display: flex;
flex-direction: column;
align-items: flex-start;
flex: 1;
}
.error-message-title {
font-size: 1.6rem;
font-weight: 500;
}
.error-message-content {
font-size: 1.4rem;
}
.error-view-trace {
font-size: 1.4rem;
cursor: pointer;
text-decoration: underline;
}
.error-notification-container {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translate(-50%, 0);
z-index: 9000;
pointer-events: none;
}
.error-notification {
min-width: 30rem;
max-width: 100rem;
padding: 1.6rem;
border-radius: 0.8rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1.6rem;
background-color: #BC2B72;
color: #ffffff;
font-size: 1.4rem;
cursor: pointer;
pointer-events: auto;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.icon-error-notification {
cursor: pointer;
transition: all 0.1s ease-in-out;
background-color: #BC2B72;
color: #ffffff;
}
.icon-error-notification:hover, .icon-error-notification:active {
cursor: pointer;
transform: scale(1.1);
}
/* Transition animations */
.error-container-enter-active {
transition: all 0.3s ease;
}
.error-container-leave-active {
transition: all 0.3s ease;
}
.error-container-enter-from {
opacity: 0;
transform: translate(-50%, 100%);
}
.error-container-leave-to {
opacity: 0;
transform: translate(-50%, 100%);
}
.error-container-move {
transition: transform 0.3s ease;
}
</style>

View file

@ -55,7 +55,7 @@ export default {
id: this.nextId++,
message: options.message || 'Notification',
title: options.title || null,
variant: options.variant || 'primary',
variant: this.mapVariant(options.variant || 'primary' ),
duration: options.duration !== undefined ? options.duration : 5000,
icon: options.icon ? `Ph${options.icon.charAt(0).toUpperCase() + options.icon.slice(1)}` : null,
};
@ -72,6 +72,21 @@ export default {
return toast.id
},
mapVariant(variant) {
switch(variant) {
case 'success':
return 'primary'
case 'warning':
return 'secondary'
case 'info':
return 'primary'
case 'error':
return 'exception'
default:
return variant;
}
},
/**
* Remove a toast by ID
* @param {number} id - Toast ID
@ -189,8 +204,8 @@ export default {
}
.toast--secondary {
background-color: #002F54;
color: #ffffff;
color: #002F54;
background-color: #c3cfdf;
}
.toast--exception{

View file

@ -148,8 +148,8 @@ export default {
rateTypeValue: "container",
selectedTypeColumns: [],
matrixColumns: [
{key: 'source.iso_code', label: 'From Country (ISO code)'},
{key: 'destination.iso_code', label: 'To Country (ISO code)'},
{key: 'source', label: 'From Country (ISO code)', formatter: (country) => {return `${country.iso_code} (${country.name})`}},
{key: 'destination', label: 'To Country (ISO code)', formatter: (country) => {return `${country.iso_code} (${country.name})`}},
{key: 'rate', align: 'right', label: 'Rate [EUR/km]'},
],
containerColumns: [

View file

@ -23,12 +23,12 @@
</div>
<div v-if="tariffUnlocked" class="tariff-rate-info-header">
<div v-if="tariffUnlocked" class="tariff-rate-info-container">
<ph-warning size="24" />
<div>
<div>Tariff rate lookup failed</div>
<div class="tariff-rate-info-headerbox">
Please contact a customs expert to obtain HS code and tariff rate.
<div>Automatic tariff rate determination was ambiguous</div>
<div class="tariff-rate-info-text">
Please contact a customs expert to obtain correct HS code and tariff rate.
</div>
</div>
</div>
@ -179,12 +179,14 @@ export default {
const multiple = 10 ** digits;
return Math.round(number * multiple) / multiple;
},
hsCodeChanged(event) {
const sanitized = event.target.value
let sanitized = event.target.value
.replace(/\D/g, '')
.substring(0, 10);
if(sanitized.length !== 10)
sanitized = null;
this.$emit("update:hsCode", sanitized);
event.target.value = sanitized;
}
@ -305,13 +307,13 @@ export default {
position: relative;
}
.tariff-rate-info-headerbox {
.tariff-rate-info-text {
color: #002F54;
font-size: 1.2rem;
font-weight: 400;
}
.tariff-rate-info-header {
.tariff-rate-info-container {
display: flex;
align-items: center;
gap: 0.8rem;

View file

@ -24,6 +24,7 @@ import ErrorModalTraceView from "@/components/layout/error/ErrorModalTraceView.v
import IconButton from "@/components/UI/IconButton.vue";
import ErrorModalOverview from "@/components/layout/error/ErrorModalOverview.vue";
import ErrorModalPiniaStore from "@/components/layout/error/ErrorModalPiniaStore.vue";
import ErrorModalRequest from "@/components/layout/error/ErrorModalRequest.vue";
export default {
name: "ErrorModal",
@ -37,24 +38,38 @@ export default {
},
computed: {
tabsError() {
return [
{
const tabs = [];
tabs.push({
title: 'Overview',
component: markRaw(ErrorModalOverview),
props: {isSelected: false, error: this.error},
},
{
});
if (this.error.request)
tabs.push({
title: 'API request',
component: markRaw(ErrorModalRequest),
props: {isSelected: false, error: this.error},
});
if (this.error.trace)
tabs.push({
title: 'Stack trace',
component: markRaw(ErrorModalTraceView),
props: {isSelected: false, error: this.error},
},
{
});
if (this.error.pinia)
tabs.push({
title: 'Frontend storage',
component: markRaw(ErrorModalPiniaStore),
props: {isSelected: false, error: this.error},
},
]
});
return tabs;
}
},
}

View file

@ -4,7 +4,6 @@
<div>
<basic-button icon="clipboard" @click="copyToClipboard">Copy error to clipboard</basic-button>
<Toast ref="toast"/>
</div>
<div class="error-section">
@ -87,12 +86,13 @@
import BasicBadge from "@/components/UI/BasicBadge.vue";
import {buildDate} from "@/common.js";
import BasicButton from "@/components/UI/BasicButton.vue";
import Toast from "@/components/UI/Toast.vue";
import {error} from "loglevel";
import logger from "@/logger.js";
import {mapStores} from "pinia";
import {useErrorStore} from "@/store/error.js";
export default {
name: "ErrorModalOverview",
components: {Toast, BasicButton, BasicBadge},
components: {BasicButton, BasicBadge},
props: {
isSelected: {
type: Boolean,
@ -108,20 +108,31 @@ export default {
try {
await navigator.clipboard.writeText(JSON.stringify(this.error));
this.$refs.toast.addToast({
this.errorStore.addNotification({
icon: 'Clipboard',
message: "The error has been copied to clipboard",
title: "Successful copied to clipboard",
variant: 'success',
duration: 4000
})
}
)
} catch (err) {
console.error('Fehler beim Kopieren:', err);
logger.error('Fehler beim Kopieren:', err);
this.errorStore.addNotification({
icon: 'Clipboard',
message: "Cannot copy to clipboard",
title: "Not copied to clipboard",
variant: 'error',
duration: 4000
}
)
}
}
},
computed: {
...mapStores(useErrorStore),
badgeIcon() {
if (this.error.type === "FRONTEND") {
return "desktop";
@ -214,6 +225,7 @@ code {
.error-label {
font-weight: 500;
font-size: 1.4rem;
align-self: start;
color: #6B869C;
text-transform: uppercase;
letter-spacing: 0.05rem;
@ -225,21 +237,8 @@ code {
word-break: break-word;
}
.error-title {
font-weight: 500;
color: #d32f2f;
}
.error-message {
color: #666;
}
.error-code {
font-family: 'Roboto Mono', monospace;
background-color: #f5f5f5;
padding: 0.8rem 1.2rem;
border-radius: 0.4rem;
border: 1px solid #e0e0e0;
.error-section:last-child {
border-bottom: none;
}
.error-code code {
@ -247,46 +246,4 @@ code {
font-size: 1.3rem;
}
.error-location {
font-family: 'Roboto Mono', monospace;
background-color: #f5f5f5;
padding: 0.8rem 1.2rem;
border-radius: 0.4rem;
border: 1px solid #e0e0e0;
}
.error-location code {
color: #1976d2;
font-size: 1.2rem;
}
.type-badge {
display: inline-block;
padding: 0.6rem 1.2rem;
border-radius: 0.4rem;
font-weight: 600;
font-size: 1.3rem;
width: fit-content;
text-transform: uppercase;
letter-spacing: 0.05rem;
}
.type-backend {
background-color: #ffebee;
color: #c62828;
border: 1px solid #ef5350;
}
.type-frontend {
background-color: #fff3e0;
color: #e65100;
border: 1px solid #ff9800;
}
.type-unknown {
background-color: #f5f5f5;
color: #757575;
border: 1px solid #bdbdbd;
}
</style>

View file

@ -0,0 +1,236 @@
<template>
<div v-if="showCard" class="error-overview-container">
<div class="error-overview">
<div class="error-section">
<div class="error-label">Method</div>
<div class="error-value">
<basic-badge :icon="requestIcon" :variant="badgeVariant">{{ requestMethod || 'UNKNOWN' }}</basic-badge>
</div>
</div>
<div class="error-section">
<div class="error-label">Url</div>
<div class="error-value">
{{ url }}
</div>
</div>
<div class="error-section">
<div class="error-label">Response expected</div>
<div class="error-value">
{{ expectResponse }}
</div>
</div>
<div class="error-section" v-if="expectedException">
<div class="error-label">Expected exceptions</div>
<div class="error-value">
{{ expectedException }}
</div>
</div>
<div class="error-section">
<div class="error-label">Header</div>
<div class="error-value">
<!-- <div class="json-display">-->
<!-- {{ formattedHeader }}-->
<!-- </div>-->
<div class="error-sub-section-container">
<div class="error-sub-section" v-for="(value, key) in params.headers" :key="key">
<div class="error-sub-label"> {{ key }}</div>
<div class="error-sub-value"> {{ value }}</div>
</div>
</div>
</div>
</div>
<div class="error-section">
<div class="error-label">Body</div>
<div class="error-value">
<div class="json-display">
{{ formattedBody }}
</div>
</div>
</div>
</div>
</div>
<div v-else class="no-data">
<span class="space-around">No request data available</span>
</div>
</template>
<script>
import BasicButton from "@/components/UI/BasicButton.vue";
import BasicBadge from "@/components/UI/BasicBadge.vue";
export default {
name: "ErrorModalRequest",
components: {BasicBadge, BasicButton},
props: {
error: {
type: Object,
required: true
}
},
computed: {
expectedException() {
return this.request.expectedException
},
expectResponse() {
return this.request.expectResponse
},
showCard() {
return (this.error != null && this.error.request != null);
},
request() {
return JSON.parse(this.error.request);
},
url() {
return this.request.url;
},
params() {
return this.request.params;
},
requestMethod() {
return this.params.method;
},
requestIcon() {
switch (this.requestMethod) {
case "GET":
return "download";
case "POST":
case "PUT":
return "upload";
case "DELETE":
return "trash";
default:
return ""
}
},
header() {
return this.params.headers;
},
body() {
return this.params.body;
},
badgeVariant() {
switch (this.requestMethod) {
case "GET":
return "primary";
case "POST":
case "PUT":
return "secondary";
case "DELETE":
default:
return "grey";
}
},
formattedBody() {
if (!this.body) return '[]';
return JSON.stringify(this.body, null, 2);
},
formattedHeader() {
if (!this.header) return '';
return JSON.stringify(this.header, null, 2);
}
}
}
</script>
<style scoped>
.error-overview-container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
overflow: hidden;
}
.error-overview {
overflow: auto;
padding: 1.6rem;
display: flex;
flex-direction: column;
gap: 1.6rem;
flex: 1;
min-height: 0;
}
.error-section {
display: grid;
grid-template-columns: 20rem 1fr;
align-items: center;
padding: 0.8rem;
gap: 0.6rem;
border-bottom: .1rem solid #E3EDFF
}
.error-sub-section-container {
width: fit-content;
}
.error-section:last-child {
border-bottom: none;
}
.error-label {
align-self: start;
font-weight: 500;
font-size: 1.4rem;
color: #6B869C;
text-transform: uppercase;
letter-spacing: 0.05rem;
}
.error-sub-label {
color: #6B869C;
font-weight: 500;
font-size: 1.4rem;
text-transform: uppercase;
}
.error-sub-value {
font-size: 1.2rem;
padding-left: 1.2rem;
padding-bottom: 0.8rem;
}
.error-value {
color: #6b7280;
font-size: 1.4rem;
word-break: break-word;
}
.no-data {
display: flex;
justify-content: center;
align-items: center;
font-size: 1.6rem;
}
.space-around {
margin: 3rem;
}
.json-display {
margin: 0;
font-family: 'Roboto mono', monospace;
font-size: 1.2rem;
flex: 1;
color: #6b7280;
white-space: pre-wrap;
word-break: break-word;
height: 100%
}
</style>

View file

@ -13,8 +13,9 @@
<div class="text-container"><input class="input-field" v-model="nodeAddress" @input="checkChange"/></div>
</div>
<div>
<basic-button icon="SealCheck" @click.prevent="verifyAddress" :disabled="addressVerified" @submit.prevent>Check address</basic-button>
<Toast ref="toast"/>
<basic-button icon="SealCheck" @click.prevent="verifyAddress" :disabled="addressVerified" @submit.prevent>
Check address
</basic-button>
</div>
<div class="input-field-caption" v-if="addressVerified">Country:</div>
<div class="country-field" v-if="addressVerified">
@ -30,7 +31,8 @@
custom-filter="grayscale(0.8) sepia(0.5) hue-rotate(180deg) saturate(0.5) brightness(1.0)"></open-street-map-embed>
</div>
<div class="create-new-node-footer">
<basic-button @click.prevent="send" variant="primary" :show-icon="false" :disabled="unverified">Create</basic-button>
<basic-button @click.prevent="send" variant="primary" :show-icon="false" :disabled="unverified">Create
</basic-button>
<basic-button @click.prevent="cancel" variant="secondary" :show-icon="false">Cancel</basic-button>
</div>
</form>
@ -43,12 +45,11 @@ import BasicButton from "@/components/UI/BasicButton.vue";
import {mapStores} from "pinia";
import {useNodeStore} from "@/store/node.js";
import OpenStreetMapEmbed from "@/components/UI/OpenStreetMapEmbed.vue";
import Toast from "@/components/UI/Toast.vue";
import Flag from "@/components/UI/Flag.vue";
export default {
name: "CreateNewNode",
components: {Flag, Toast, OpenStreetMapEmbed, BasicButton},
components: {Flag, OpenStreetMapEmbed, BasicButton},
emits: ['created', 'close'],
data() {
return {
@ -114,15 +115,7 @@ export default {
this.addressVerified = false;
this.verifiedAddress = null;
if (error !== null) {
this.$refs.toast.addToast({
icon: 'warning',
message: error.message,
title: "Cannot locate address.",
variant: 'exception',
duration: 8000
})
} else if (node) {
if (error === null && node !== null) {
this.nodeCoordinates = node.location;
this.nodeAddress = node.address;
@ -132,7 +125,6 @@ export default {
this.addressVerified = true;
this.verifiedAddress = node.address;
this.node = node;
} else {
}
},

View file

@ -1,6 +1,8 @@
<template>
<div class="start-calculation-container">
<h2 class="page-header">Create Calculation</h2>
<div class="part-numbers-headers">
<h3 class="sub-header">Part Numbers</h3>
<basic-button icon="CloudArrowUp" :showIcon="true" @click="activateModal('partNumber')">Drop part numbers

View file

@ -17,7 +17,6 @@
</div>
</div>
<Toast ref="toast"/>
<div v-if="premiseSingleEditStore.showLoadingSpinner" class="edit-calculation-spinner-container">
<box class="edit-calculation-spinner">
@ -112,14 +111,14 @@ import NotificationBar from "@/components/UI/NotificationBar.vue";
import Modal from "@/components/UI/Modal.vue";
import TraceView from "@/components/layout/TraceView.vue";
import IconButton from "@/components/UI/IconButton.vue";
import Toast from "@/components/UI/Toast.vue";
import {UrlSafeBase64} from "@/common.js";
import {usePremiseSingleEditStore} from "@/store/premiseSingleEdit.js";
import logger from "@/logger.js";
export default {
name: "SingleEdit",
components: {
Toast,
IconButton,
TraceView,
Modal,
@ -157,7 +156,6 @@ export default {
return "Please wait. Routing ..."
return "Please wait. Calculating ...";
}
},
methods: {
@ -166,28 +164,7 @@ export default {
this.showCalculationModal = true;
const error = await this.premiseSingleEditStore.startCalculation();
if (error !== null) {
if (error.title === 'Internal Server Error') {
this.$refs.toast.addToast({
icon: 'warning',
message: error.message,
title: "Calculation finished with errors",
variant: 'exception',
duration: 8000
});
this.close();
} else {
this.$refs.toast.addToast({
icon: 'warning',
message: error.message,
title: "Cannot start calculation",
variant: 'exception',
duration: 8000
});
}
} else {
if (error === null) {
this.close();
}
@ -204,25 +181,13 @@ export default {
}
},
async save(type) {
let success = false;
if (type === 'price') {
success = await this.premiseSingleEditStore.savePrice();
await this.premiseSingleEditStore.savePrice();
} else if (type === 'material') {
success = await this.premiseSingleEditStore.saveMaterial();
await this.premiseSingleEditStore.saveMaterial();
} else if (type === 'packaging') {
success = await this.premiseSingleEditStore.savePackaging();
await this.premiseSingleEditStore.savePackaging();
}
if (!success)
this.$refs.toast.addToast({
icon: 'warning',
message: "Failed to save data.",
title: "Error saving",
variant: 'exception',
duration: 8000
});
},
trace() {
this.traceModal = true;
@ -234,6 +199,7 @@ export default {
if (this.$route.params.ids)
this.bulkEditQuery = this.$route.params.ids;
if (!isNaN(this.id))
this.premiseSingleEditStore.load(this.id)
},
@ -305,11 +271,6 @@ export default {
}
}
.trace-link {
cursor: pointer;
text-decoration: underline;
}
.space-around {
margin: 3rem;
}
@ -333,7 +294,4 @@ export default {
align-items: center;
}
.destination-list-container {
margin: 2.4rem 1.6rem;
}
</style>

View file

@ -2,28 +2,48 @@ import {defineStore, getActivePinia} from 'pinia'
import {config} from '@/config'
import {toRaw} from "vue";
import {getCsrfToken} from "@/backend.js";
import logger from "@/logger.js";
export const useErrorStore = defineStore('error', {
state() {
return {
errors: [],
notifications: [],
sendCache: [],
autoSubmitInterval: 30000,
autoSubmitTimer: null
}
},
getters: {
lastError: (state) => state.errors.length > 0 ? state.errors[state.errors.length - 1].error : null,
lastError: (state) => state.notifications.length > 0 ? state.notifications[state.notifications.length - 1].error : null,
},
actions: {
clearErrors() {
this.errors = [];
this.notifications = [];
console.log("Cleared errors");
},
popNotification() {
return this.notifications.pop();
},
addNotification(notification) {
this.notifications.push({
icon: notification.icon ?? null,
message: notification.message ?? 'Unknown notification',
title: notification.title ?? 'Notification',
variant: notification.variant ?? 'success',
duration: notification.duration ?? 8000
})
},
async addError(errorDto, options = {}) {
const {request = null, store = null, global = false} = options;
const state = this.captureStoreState(store, global);
this.addNotification({
icon: 'bug',
message: errorDto.message ?? 'Unknown error code',
title: errorDto.title ?? 'Unknown error',
variant: 'exception',
});
const error = {
error: {
code: errorDto.code ?? 'Unknown error code',
@ -37,9 +57,8 @@ export const useErrorStore = defineStore('error', {
timestamp: Date.now()
}
console.log(error);
logger.log(error);
this.errors.push(error);
this.sendCache.push(error);
await this.transmitErrors();
},
@ -57,7 +76,6 @@ export const useErrorStore = defineStore('error', {
const url = `${config.backendUrl}/error/`;
const response = await fetch(url, params
).catch(e => {
this.startAutoSubmitTimer();
@ -117,7 +135,7 @@ export const useErrorStore = defineStore('error', {
},
async submitOnBeforeUnload() {
if (this.sendCache.length > 0) {
navigator.sendBeacon('/api/errors', JSON.stringify(
navigator.sendBeacon(`${config.backendUrl}/error/`, JSON.stringify(
toRaw(this.sendCache)
))
}
@ -132,30 +150,30 @@ export function setupErrorBuffer() {
const errorStore = useErrorStore()
//Unhandled Promise Rejections
// window.addEventListener('unhandledrejection', (event) => {
window.addEventListener('unhandledrejection', (event) => {
// const error = {
// code: "Unhandled rejection",
// title: "Frontend error",
// message: event.reason?.message || 'Unhandled Promise Rejection',
// traceCombined: event.reason?.stack,
// };
const error = {
code: "Unhandled rejection",
title: "Frontend error",
message: event.reason?.message || 'Unhandled Promise Rejection',
traceCombined: event.reason?.stack,
};
// errorStore.addError(error, {global: true}).then(r => {} );
// })
errorStore.addError(error, {global: true}).then(r => {} );
})
// // JavaScript Errors
// window.addEventListener('error', (event) => {
// const error = {
// code: "General error",
// title: "Frontend error",
// message: event.reason?.message || 'Unhandled Promise Rejection',
// traceCombined: event.reason?.stack,
// };
// errorStore.addError(error, {global: true}).then(r => {} );
// })
window.addEventListener('error', (event) => {
const error = {
code: "General error",
title: "Frontend error",
message: event.reason?.message || 'Unhandled Promise Rejection',
traceCombined: event.reason?.stack,
};
errorStore.addError(error, {global: true}).then(r => {} );
})
window.addEventListener('beforeunload', () => {
errorStore.submitOnBeforeUnload();
window.addEventListener('beforeunload', async () => {
await errorStore.submitOnBeforeUnload();
})
}

View file

@ -35,7 +35,6 @@ export const useNodeStore = defineStore('node', {
let error = null;
params.append('address', address);
const data = await performRequest(this, "GET", `${config.backendUrl}/nodes/locate/${params.size === 0 ? '' : '?'}${params.toString()}`, null, true, 'Geocoding error').catch(e => {
logger.log("geo locate exception", e.errorObj);
error = e.errorObj;
});
return {node: data?.data, error: error};

View file

@ -53,6 +53,8 @@ export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', {
this.loading = true;
this.premise = null;
logger.info("load premise", id);
const params = new URLSearchParams();
params.append('premissIds', id.toString());
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
@ -71,8 +73,7 @@ export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', {
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);
await performRequest(this, 'PUT', url, body, false, ['Premiss validation error']).catch(e => {
error = e.errorObj;
})

View file

@ -20,6 +20,7 @@ import org.springframework.web.method.annotation.MethodArgumentTypeMismatchExcep
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;
@ControllerAdvice
@ -27,7 +28,7 @@ public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ErrorResponseDTO> handleValidationExceptions(
public ResponseEntity<ErrorResponseDTO> handleMalformedReqExceptions(
HttpMessageNotReadableException exception) {
ErrorDTO error = new ErrorDTO(
@ -59,7 +60,7 @@ public class GlobalExceptionHandler {
// You might want to create a custom ErrorDTO that can handle field errors
ErrorDTO error = new ErrorDTO(
exception.getClass().getName(),
"Validation Failed",
"Validation failed",
errorMessage,
Arrays.asList(exception.getStackTrace())
);
@ -113,7 +114,6 @@ public class GlobalExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponseDTO> handleConstraintViolation(ConstraintViolationException exception) {
// Extract constraint violation details
String errorMessage = exception.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
@ -153,12 +153,30 @@ public class GlobalExceptionHandler {
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(CompletionException.class)
public ResponseEntity<ErrorResponseDTO> handleCompletionException(CompletionException exception) {
Throwable cause = exception.getCause();
if (cause instanceof PremiseValidationError) {
return handlePremiseValidationException((PremiseValidationError) cause);
} else if (cause instanceof GeocodingException) {
return handleGeocodingException((GeocodingException) cause);
} else if (cause instanceof BadRequestException) {
return handleMethodArgumentNotValid((BadRequestException) cause);
}
return handleGenericException(exception);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponseDTO> handlePremiseValidationException(Exception exception) {
public ResponseEntity<ErrorResponseDTO> handleGenericException(Exception exception) {
Throwable cause = exception.getCause();
ErrorDTO error = new ErrorDTO(
exception.getClass().getName(),
cause != null ? cause.getClass().getName() : exception.getClass().getName(),
"Internal Server Error",
exception.getMessage(),
cause != null ? cause.getMessage() : exception.getMessage(),
Arrays.asList(exception.getStackTrace())
);

View file

@ -23,6 +23,8 @@ import java.util.List;
@RequestMapping("/api/reports")
public class ReportingController {
//TODO: rollenbeschränkung
private final ReportingService reportingService;
private final ExcelReportingService excelReportingService;

View file

@ -16,6 +16,8 @@ public class ErrorLogDTO {
private String pinia;
private String request;
@JsonProperty("calculation_job_id")
private Integer calculationJobId;
@ -29,6 +31,14 @@ public class ErrorLogDTO {
@JsonProperty("timestamp")
private LocalDateTime createdAt;
public String getRequest() {
return request;
}
public void setRequest(String request) {
this.request = request;
}
public Integer getUserId() {
return userId;
}

View file

@ -62,6 +62,8 @@ public class SysError {
private LocalDateTime createdAt;
private String request;
/**
* Gets the unique identifier of the system error.
*
@ -249,4 +251,12 @@ public class SysError {
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public String getRequest() {
return request;
}
public void setRequest(String request) {
this.request = request;
}
}

View file

@ -4,182 +4,184 @@ import java.util.Optional;
public enum MeasureType {
// Serie D - Anti-dumping or countervailing duties (Anti-Dumping/Ausgleichszölle)
DUMPP("551", true, "D"), // Provisional anti-dumping duty - Vorläufiger Anti-Dumping-Zoll
DUMPD("552", true, "D"), // Definitive anti-dumping duty - Endgültiger Anti-Dumping-Zoll
COMPP("553", true, "D"), // Provisional countervailing duty - Vorläufiger Ausgleichszoll
COMPD("554", true, "D"), // Definitive countervailing duty - Endgültiger Ausgleichszoll
PCDUM("555", true, "D"), // Anti-dumping/countervailing duty - Pending collection - Schwebende Erhebung
NTDUM("561", false, "D"), // Notice of initiation of anti-dumping proceeding - Verfahrenseröffnung
SPDUM("562", true, "D"), // Suspended anti-dumping/countervailing duty - Ausgesetzter Zoll
RGDUM("564", false, "D"), // Anti-dumping/countervailing registration - Registrierung
REDUM("565", false, "D"), // Anti-dumping/countervailing review - Überprüfung
STDUM("566", false, "D"), // Anti-dumping/countervailing statistic - Statistik
CTDUM("570", false, "D"), // Anti-dumping/countervailing duty - Control - Kontrolle
// Serie C - Applicable duty (Anwendbare Zollsätze)
APPL("103", true), // Third country duty - Standard-Drittlandszollsatz
APPEU("105", true), // Non preferential duty under end-use - Zollsatz unter End-Use
CUD("106", true), // Customs Union Duty - Zollunionszollsatz
S("112", true), // Autonomous tariff suspension - Autonome Zollaussetzung
SUSEU("115", true), // Autonomous suspension under end-use - Aussetzung unter End-Use
SUSSH("117", false), // Suspension - ships/boats/vessels - Aussetzung für Schiffe (nicht relevant)
AIRWO("119", false), // Airworthiness tariff suspension - Lufttüchtigkeits-Zollaussetzung (nicht relevant)
K_122("122", true), // Non preferential tariff quota - Nichtpräferenzielles Zollkontingent
KEU("123", true), // Non preferential tariff quota under end-use - Kontingent unter End-Use
OPQ("140", true), // Outward processing tariff preference - Passive Veredelung
S_141("141", true), // Preferential suspension - Präferenzielle Aussetzung
PREF("142", true), // Tariff preference - Zollpräferenz
K_143("143", true), // Preferential tariff quota - Präferenzielles Zollkontingent
C("144", true), // Preferential ceiling - Präferenzplafond
PRFEU("145", true), // Preference under end-use - Präferenz unter End-Use
PRKEU("146", true), // Preferential tariff quota under end-use - Präferenzkontingent unter End-Use
CUQ("147", true), // Customs Union Quota - Zollunionskontingent
APPL("103", true, "C"), // Third country duty - Standard-Drittlandszollsatz
APPEU("105", true, "C"), // Non preferential duty under end-use - Zollsatz unter End-Use
CUD("106", true, "C"), // Customs Union Duty - Zollunionszollsatz
S("112", true, "C"), // Autonomous tariff suspension - Autonome Zollaussetzung
SUSEU("115", true, "C"), // Autonomous suspension under end-use - Aussetzung unter End-Use
SUSSH("117", false, "C"), // Suspension - ships/boats/vessels - Aussetzung für Schiffe (nicht relevant)
AIRWO("119", false, "C"), // Airworthiness tariff suspension - Lufttüchtigkeits-Zollaussetzung (nicht relevant)
K_122("122", true, "C"), // Non preferential tariff quota - Nichtpräferenzielles Zollkontingent
KEU("123", true, "C"), // Non preferential tariff quota under end-use - Kontingent unter End-Use
OPQ("140", true, "C"), // Outward processing tariff preference - Passive Veredelung
S_141("141", true, "C"), // Preferential suspension - Präferenzielle Aussetzung
PREF("142", true, "C"), // Tariff preference - Zollpräferenz
K_143("143", true, "C"), // Preferential tariff quota - Präferenzielles Zollkontingent
C("144", true, "C"), // Preferential ceiling - Präferenzplafond
PRFEU("145", true, "C"), // Preference under end-use - Präferenz unter End-Use
PRKEU("146", true, "C"), // Preferential tariff quota under end-use - Präferenzkontingent unter End-Use
CUQ("147", true, "C"), // Customs Union Quota - Zollunionskontingent
// Serie A - Import/Export prohibited (Verbote)
PRO("277", false), // Import prohibition - Einfuhrverbot
PRX("278", false), // Export prohibition - Ausfuhrverbot
RSUBH("481", false), // Declaration of subheading submitted to restrictions (import) - Einfuhrbeschränkung
RSHEX("485", false), // Declaration of subheading submitted to restrictions (export) - Ausfuhrbeschränkung
PRO("277", false, "A"), // Import prohibition - Einfuhrverbot
PRX("278", false, "A"), // Export prohibition - Ausfuhrverbot
RSUBH("481", false, "A"), // Declaration of subheading submitted to restrictions (import) - Einfuhrbeschränkung
RSHEX("485", false, "A"), // Declaration of subheading submitted to restrictions (export) - Ausfuhrbeschränkung
// Serie P - VAT (Mehrwertsteuer)
TVA("305", false), // Value added tax - Mehrwertsteuer
TVA("305", false, "P"), // Value added tax - Mehrwertsteuer
// Serie Q - Excises (Verbrauchssteuern)
ACC("306", false), // Excises - Verbrauchssteuern
ACC("306", false, "Q"), // Excises - Verbrauchssteuern
// Serie O - Supplementary unit (Besondere Maßeinheiten)
SUPUN("109", false), // Supplementary unit - Besondere Maßeinheit
USIMP("110", false), // Supplementary unit import - Besondere Maßeinheit Einfuhr
USEXP("111", false), // Supplementary unit export - Besondere Maßeinheit Ausfuhr
SUPUN("109", false, "O"), // Supplementary unit - Besondere Maßeinheit
USIMP("110", false, "O"), // Supplementary unit import - Besondere Maßeinheit Einfuhr
USEXP("111", false, "O"), // Supplementary unit export - Besondere Maßeinheit Ausfuhr
// Serie B - Entry subject to conditions (Einfuhr unter Bedingungen)
VETCTR("410", false), // Veterinary control - Veterinärkontrolle (nicht relevant für Gabelstapler)
PRIOR_SURV("420", true), // Entry into free circulation (prior surveillance) - Vorherige Überwachung
REU("464", true), // Declaration of subheading submitted to end-use provisions - End-Use-Erklärung
LPS("465", true), // Restriction on entry into free circulation - Einfuhrbeschränkung
SPX("467", false), // Restriction on export - Ausfuhrbeschränkung
QX("473", false), // Export authorization - Ausfuhrgenehmigung
LPQ("474", true), // Entry into free circulation (quantitative limitation) - Mengenmäßige Beschränkung
LPR("475", true), // Restriction on entry into free circulation - Einfuhrbeschränkung
RX("476", false), // Restriction on export - Ausfuhrbeschränkung
OPT("477", true), // Entry into free circulation (outward processing traffic) - Passive Veredelung
DURX("478", false), // Export authorization (Dual use) - Ausfuhrgenehmigung Dual-Use
CHMEX("479", false), // Export control on dangerous chemicals - Exportkontrolle gefährliche Chemikalien
LCRED_M1("482", false), // Declaration submitted to legal restrictions (net weight/supplementary unit) - Rechtliche Beschränkungen
LCRED_V1("483", false), // Declaration submitted to legal restrictions (unit price) - Rechtliche Beschränkungen
PCRED_M1("484", false), // Declaration submitted to physical restrictions (net weight/supplementary unit) - Physische Beschränkungen
PCRED_V("491", false), // Declaration submitted to physical restrictions (declared statistical value) - Physische Beschränkungen
PCRED_M("492", false), // Declaration submitted to physical restrictions (declared net mass) - Physische Beschränkungen
PCRED_S("493", false), // Declaration submitted to physical restrictions (declared supplementary unit) - Physische Beschränkungen
LCRED_V("494", false), // Declaration submitted to legal restrictions (declared statistical value) - Rechtliche Beschränkungen
LCRED_M("495", false), // Declaration submitted to legal restrictions (declared net mass) - Rechtliche Beschränkungen
LCRED_S("496", false), // Declaration submitted to legal restrictions (declared supplementary unit) - Rechtliche Beschränkungen
TRIMP("705", false), // Import prohibition on goods for torture and repression - Folter-/Repressionsgüter
TREXP("706", false), // Export prohibition - torture/repression goods - Ausfuhrverbot Folter-/Repressionsgüter
IMPORT_CTRL("707", true), // Import control - Einfuhrkontrolle (potentiell relevant für Bauteile)
TRRX("708", false), // Export restriction - torture/repression goods - Ausfuhrbeschränkung
EXPCTRL("709", false), // Export control - Ausfuhrkontrolle
CITESIMP("710", false), // Import control - CITES - CITES-Kontrolle (nicht relevant)
MILIM("711", false), // Import control on restricted goods and technologies - Militärgüter (nicht relevant)
IAS("712", false), // Import control - IAS - Invasive Arten (nicht relevant)
IMPORT_CTRL_GMO("713", false), // Import control on GMOs - GMO-Kontrolle (nicht relevant)
IMPCTRL("714", true), // Import control - Einfuhrkontrolle (potentiell relevant)
CITESEX("715", false), // Export control - CITES - CITES-Ausfuhrkontrolle
CFISH("716", false), // Export control - Fish - Fischereikontrolle (nicht relevant)
MILEX("717", false), // Export control on restricted goods and technologies - Militärgüter
LUXEX("718", false), // Export control on luxury goods - Luxusgüter (nicht relevant)
CIUUF("719", false), // Control on illegal fishing - Illegale Fischerei (nicht relevant)
FFIM("722", false), // Entry restriction - feed and food - Futter-/Lebensmittel (nicht relevant)
FGGIMP("724", false), // Import control of fluorinated greenhouse gases - Treibhausgase
OZEXP("725", false), // Export control on ozone-depleting substances - Ozonschicht
OZIMP("726", false), // Import control on ozone-depleting substances - Ozonschicht
LUXIM("728", false), // Import control on luxury goods - Luxusgüter (nicht relevant)
EXPCHE("730", false), // Compliance with pre-export checks requirements - Vorausfuhrkontrollen
IMPORT_CTRL_731("731", true), // Import control - Einfuhrkontrolle (potentiell relevant)
IMPORT_CTRL_732("732", true), // Import control - Einfuhrkontrolle (potentiell relevant)
CGIMP("734", false), // Import control on cultural goods - Kulturgüter (nicht relevant)
CGEXP("735", false), // Export control on cultural goods - Kulturgüter (nicht relevant)
EXPORT_CTRL_736("736", false), // Export control - Ausfuhrkontrolle
EXPORT_CTRL_737("737", false), // Export control - Ausfuhrkontrolle
IMPORT_EXPORT_CML("738", false), // Import/export restriction - Common Military List goods - Militärgüter
FUREX("740", false), // Export control on cat and dog fur - Katzen-/Hundefell (nicht relevant)
FURIM("745", false), // Import control on cat and dog fur - Katzen-/Hundefell (nicht relevant)
SEAL("746", false), // Import control on seal products - Robbenprodukte (nicht relevant)
FLEGT("747", false), // Import control of timber - FLEGT licensing - Holzprodukte (nicht relevant)
MERIM("748", false), // Import control of mercury - Quecksilber
MEREX("749", false), // Export control of mercury - Quecksilber
ORGANIC("750", false), // Import control of organic products - Bio-Produkte (nicht relevant)
WSTEX("751", false), // Export control - Waste - Abfall
WSTIM("755", false), // Import control - Waste - Abfall
IMPCTRL_760("760", true), // Import control - Einfuhrkontrolle (potentiell relevant)
CHMIM("761", false), // Import control on REACH - Chemikalien
IMPORT_CTRL_762("762", true), // Import control - Einfuhrkontrolle (potentiell relevant)
IMPORT_CTRL_763("763", true), // Import control - Einfuhrkontrolle (potentiell relevant)
FGEXP("765", false), // Export control of fluorinated greenhouse gases - Treibhausgase
EXPORT_CTRL_766("766", false), // Export control - Ausfuhrkontrolle
EXPORT_CTRL_767("767", false), // Export control - Ausfuhrkontrolle
EXPORT_CTRL_768("768", false), // Export control - Ausfuhrkontrolle
IMPOP("769", false), // Import control on persistent organic pollutants - Organische Schadstoffe
FLEGT_GHANA("770", false), // Import control timber - FLEGT Ghana - Holzprodukte (nicht relevant)
FLEGT_CAMEROON("771", false), // Import control timber - FLEGT Cameroon - Holzprodukte (nicht relevant)
FLEGT_CONGO("772", false), // Import control timber - FLEGT Congo - Holzprodukte (nicht relevant)
FLEGT_CAR("773", false), // Import control timber - FLEGT Central Africa - Holzprodukte (nicht relevant)
FLEGT_VIETNAM("774", false), // Import control timber - FLEGT Vietnam - Holzprodukte (nicht relevant)
CBAM("775", false), // Carbon Border Adjustment Mechanism - CO2-Grenzausgleich
DFRSTIM("776", false), // Import control on deforestation - Entwaldung
DFRSEXP("777", false), // Export control on deforestation - Entwaldung
EXPORT_CTRL_780("780", false), // Export control - Ausfuhrkontrolle
EXPORT_CTRL_781("781", false), // Export control - Ausfuhrkontrolle
VETCTR("410", false, "B"), // Veterinary control - Veterinärkontrolle (nicht relevant für Gabelstapler)
PRIOR_SURV("420", true, "B"), // Entry into free circulation (prior surveillance) - Vorherige Überwachung
REU("464", true, "B"), // Declaration of subheading submitted to end-use provisions - End-Use-Erklärung
LPS("465", true, "B"), // Restriction on entry into free circulation - Einfuhrbeschränkung
SPX("467", false, "B"), // Restriction on export - Ausfuhrbeschränkung
QX("473", false, "B"), // Export authorization - Ausfuhrgenehmigung
LPQ("474", true, "B"), // Entry into free circulation (quantitative limitation) - Mengenmäßige Beschränkung
LPR("475", true, "B"), // Restriction on entry into free circulation - Einfuhrbeschränkung
RX("476", false, "B"), // Restriction on export - Ausfuhrbeschränkung
OPT("477", true, "B"), // Entry into free circulation (outward processing traffic) - Passive Veredelung
DURX("478", false, "B"), // Export authorization (Dual use) - Ausfuhrgenehmigung Dual-Use
CHMEX("479", false, "B"), // Export control on dangerous chemicals - Exportkontrolle gefährliche Chemikalien
LCRED_M1("482", false, "B"), // Declaration submitted to legal restrictions (net weight/supplementary unit) - Rechtliche Beschränkungen
LCRED_V1("483", false, "B"), // Declaration submitted to legal restrictions (unit price) - Rechtliche Beschränkungen
PCRED_M1("484", false, "B"), // Declaration submitted to physical restrictions (net weight/supplementary unit) - Physische Beschränkungen
PCRED_V("491", false, "B"), // Declaration submitted to physical restrictions (declared statistical value) - Physische Beschränkungen
PCRED_M("492", false, "B"), // Declaration submitted to physical restrictions (declared net mass) - Physische Beschränkungen
PCRED_S("493", false, "B"), // Declaration submitted to physical restrictions (declared supplementary unit) - Physische Beschränkungen
LCRED_V("494", false, "B"), // Declaration submitted to legal restrictions (declared statistical value) - Rechtliche Beschränkungen
LCRED_M("495", false, "B"), // Declaration submitted to legal restrictions (declared net mass) - Rechtliche Beschränkungen
LCRED_S("496", false, "B"), // Declaration submitted to legal restrictions (declared supplementary unit) - Rechtliche Beschränkungen
TRIMP("705", false, "B"), // Import prohibition on goods for torture and repression - Folter-/Repressionsgüter
TREXP("706", false, "B"), // Export prohibition - torture/repression goods - Ausfuhrverbot Folter-/Repressionsgüter
IMPORT_CTRL("707", true, "B"), // Import control - Einfuhrkontrolle (potentiell relevant für Bauteile)
TRRX("708", false, "B"), // Export restriction - torture/repression goods - Ausfuhrbeschränkung
EXPCTRL("709", false, "B"), // Export control - Ausfuhrkontrolle
CITESIMP("710", false, "B"), // Import control - CITES - CITES-Kontrolle (nicht relevant)
MILIM("711", false, "B"), // Import control on restricted goods and technologies - Militärgüter (nicht relevant)
IAS("712", false, "B"), // Import control - IAS - Invasive Arten (nicht relevant)
IMPORT_CTRL_GMO("713", false, "B"), // Import control on GMOs - GMO-Kontrolle (nicht relevant)
IMPCTRL("714", true, "B"), // Import control - Einfuhrkontrolle (potentiell relevant)
CITESEX("715", false, "B"), // Export control - CITES - CITES-Ausfuhrkontrolle
CFISH("716", false, "B"), // Export control - Fish - Fischereikontrolle (nicht relevant)
MILEX("717", false, "B"), // Export control on restricted goods and technologies - Militärgüter
LUXEX("718", false, "B"), // Export control on luxury goods - Luxusgüter (nicht relevant)
CIUUF("719", false, "B"), // Control on illegal fishing - Illegale Fischerei (nicht relevant)
FFIM("722", false, "B"), // Entry restriction - feed and food - Futter-/Lebensmittel (nicht relevant)
FGGIMP("724", false, "B"), // Import control of fluorinated greenhouse gases - Treibhausgase
OZEXP("725", false, "B"), // Export control on ozone-depleting substances - Ozonschicht
OZIMP("726", false, "B"), // Import control on ozone-depleting substances - Ozonschicht
LUXIM("728", false, "B"), // Import control on luxury goods - Luxusgüter (nicht relevant)
EXPCHE("730", false, "B"), // Compliance with pre-export checks requirements - Vorausfuhrkontrollen
IMPORT_CTRL_731("731", true, "B"), // Import control - Einfuhrkontrolle (potentiell relevant)
IMPORT_CTRL_732("732", true, "B"), // Import control - Einfuhrkontrolle (potentiell relevant)
CGIMP("734", false, "B"), // Import control on cultural goods - Kulturgüter (nicht relevant)
CGEXP("735", false, "B"), // Export control on cultural goods - Kulturgüter (nicht relevant)
EXPORT_CTRL_736("736", false, "B"), // Export control - Ausfuhrkontrolle
EXPORT_CTRL_737("737", false, "B"), // Export control - Ausfuhrkontrolle
IMPORT_EXPORT_CML("738", false, "B"), // Import/export restriction - Common Military List goods - Militärgüter
FUREX("740", false, "B"), // Export control on cat and dog fur - Katzen-/Hundefell (nicht relevant)
FURIM("745", false, "B"), // Import control on cat and dog fur - Katzen-/Hundefell (nicht relevant)
SEAL("746", false, "B"), // Import control on seal products - Robbenprodukte (nicht relevant)
FLEGT("747", false, "B"), // Import control of timber - FLEGT licensing - Holzprodukte (nicht relevant)
MERIM("748", false, "B"), // Import control of mercury - Quecksilber
MEREX("749", false, "B"), // Export control of mercury - Quecksilber
ORGANIC("750", false, "B"), // Import control of organic products - Bio-Produkte (nicht relevant)
WSTEX("751", false, "B"), // Export control - Waste - Abfall
WSTIM("755", false, "B"), // Import control - Waste - Abfall
IMPCTRL_760("760", true, "B"), // Import control - Einfuhrkontrolle (potentiell relevant)
CHMIM("761", false, "B"), // Import control on REACH - Chemikalien
IMPORT_CTRL_762("762", true, "B"), // Import control - Einfuhrkontrolle (potentiell relevant)
IMPORT_CTRL_763("763", true, "B"), // Import control - Einfuhrkontrolle (potentiell relevant)
FGEXP("765", false, "B"), // Export control of fluorinated greenhouse gases - Treibhausgase
EXPORT_CTRL_766("766", false, "B"), // Export control - Ausfuhrkontrolle
EXPORT_CTRL_767("767", false, "B"), // Export control - Ausfuhrkontrolle
EXPORT_CTRL_768("768", false, "B"), // Export control - Ausfuhrkontrolle
IMPOP("769", false, "B"), // Import control on persistent organic pollutants - Organische Schadstoffe
FLEGT_GHANA("770", false, "B"), // Import control timber - FLEGT Ghana - Holzprodukte (nicht relevant)
FLEGT_CAMEROON("771", false, "B"), // Import control timber - FLEGT Cameroon - Holzprodukte (nicht relevant)
FLEGT_CONGO("772", false, "B"), // Import control timber - FLEGT Congo - Holzprodukte (nicht relevant)
FLEGT_CAR("773", false, "B"), // Import control timber - FLEGT Central Africa - Holzprodukte (nicht relevant)
FLEGT_VIETNAM("774", false, "B"), // Import control timber - FLEGT Vietnam - Holzprodukte (nicht relevant)
CBAM("775", false, "B"), // Carbon Border Adjustment Mechanism - CO2-Grenzausgleich
DFRSTIM("776", false, "B"), // Import control on deforestation - Entwaldung
DFRSEXP("777", false, "B"), // Export control on deforestation - Entwaldung
EXPORT_CTRL_780("780", false, "B"), // Export control - Ausfuhrkontrolle
EXPORT_CTRL_781("781", false, "B"), // Export control - Ausfuhrkontrolle
// Serie N - Posterior surveillance (Nachträgliche Überwachung)
PIM("440", false), // Public Import Monitoring - Öffentliche Einfuhrüberwachung
PEM("445", false), // Public Export Monitoring - Öffentliche Ausfuhrüberwachung
SUB("450", false), // Statistical surveillance - Statistische Überwachung
SUA("460", false), // Computer surveillance - Computerüberwachung
SQR("461", false), // Community surveillance - reference quantities - Referenzmengen
SUR("462", false), // Posterior import surveillance - Nachträgliche Einfuhrüberwachung
SUX("463", false), // Posterior export surveillance - Nachträgliche Ausfuhrüberwachung
SUREX("470", false), // Export surveillance - Ausfuhrüberwachung
SUEX("471", false), // Export surveillance (TQS) - Ausfuhrüberwachung
IMPMNTR_702("702", false), // Import Monitoring - Einfuhrüberwachung
IMPMNTR_703("703", false), // Import Monitoring - Einfuhrüberwachung
IMPMNTR_704("704", false), // Import Monitoring - Einfuhrüberwachung
PIM("440", false, "N"), // Public Import Monitoring - Öffentliche Einfuhrüberwachung
PEM("445", false, "N"), // Public Export Monitoring - Öffentliche Ausfuhrüberwachung
SUB("450", false, "N"), // Statistical surveillance - Statistische Überwachung
SUA("460", false, "N"), // Computer surveillance - Computerüberwachung
SQR("461", false, "N"), // Community surveillance - reference quantities - Referenzmengen
SUR("462", false, "N"), // Posterior import surveillance - Nachträgliche Einfuhrüberwachung
SUX("463", false, "N"), // Posterior export surveillance - Nachträgliche Ausfuhrüberwachung
SUREX("470", false, "N"), // Export surveillance - Ausfuhrüberwachung
SUEX("471", false, "N"), // Export surveillance (TQS) - Ausfuhrüberwachung
IMPMNTR_702("702", false, "N"), // Import Monitoring - Einfuhrüberwachung
IMPMNTR_703("703", false, "N"), // Import Monitoring - Einfuhrüberwachung
IMPMNTR_704("704", false, "N"), // Import Monitoring - Einfuhrüberwachung
// Serie M - Unit price, standard import value (Preise und Werte)
VU("488", false), // Unit price - Stückpreis
REPSU("489", false), // Representative price - Repräsentativer Preis
SIV("490", false), // Standard import value - Standard-Einfuhrwert
// Serie D - Anti-dumping or countervailing duties (Anti-Dumping/Ausgleichszölle)
DUMPP("551", true), // Provisional anti-dumping duty - Vorläufiger Anti-Dumping-Zoll
DUMPD("552", true), // Definitive anti-dumping duty - Endgültiger Anti-Dumping-Zoll
COMPP("553", true), // Provisional countervailing duty - Vorläufiger Ausgleichszoll
COMPD("554", true), // Definitive countervailing duty - Endgültiger Ausgleichszoll
PCDUM("555", true), // Anti-dumping/countervailing duty - Pending collection - Schwebende Erhebung
NTDUM("561", false), // Notice of initiation of anti-dumping proceeding - Verfahrenseröffnung
SPDUM("562", true), // Suspended anti-dumping/countervailing duty - Ausgesetzter Zoll
RGDUM("564", false), // Anti-dumping/countervailing registration - Registrierung
REDUM("565", false), // Anti-dumping/countervailing review - Überprüfung
STDUM("566", false), // Anti-dumping/countervailing statistic - Statistik
CTDUM("570", false), // Anti-dumping/countervailing duty - Control - Kontrolle
VU("488", false, "M"), // Unit price - Stückpreis
REPSU("489", false, "M"), // Representative price - Repräsentativer Preis
SIV("490", false, "M"), // Standard import value - Standard-Einfuhrwert
// Serie S - Supplementary amount (Zusätzliche Beträge)
ASURE("651", false), // Security based on representative price - Sicherheit basierend auf Preis
ASUCI("652", false), // Additional duty based on CIF price - Zusatzzoll basierend auf CIF-Preis
SECQ("653", false), // Security reduced under tariff quota - Reduzierte Sicherheit
ADQ("654", false), // Additional duty reduced under tariff quota - Reduzierter Zusatzzoll
SECPO("655", false), // Security (poultry) based on representative price - Sicherheit Geflügel
ADPOC("656", false), // Additional duty (poultry) based on CIF price - Zusatzzoll Geflügel
RESEC("657", false), // Reduced security based on representative price - Reduzierte Sicherheit
READ("658", false), // Reduced additional duty based on CIF price - Reduzierter Zusatzzoll
ASURE("651", false, "S"), // Security based on representative price - Sicherheit basierend auf Preis
ASUCI("652", false, "S"), // Additional duty based on CIF price - Zusatzzoll basierend auf CIF-Preis
SECQ("653", false, "S"), // Security reduced under tariff quota - Reduzierte Sicherheit
ADQ("654", false, "S"), // Additional duty reduced under tariff quota - Reduzierter Zusatzzoll
SECPO("655", false, "S"), // Security (poultry) based on representative price - Sicherheit Geflügel
ADPOC("656", false, "S"), // Additional duty (poultry) based on CIF price - Zusatzzoll Geflügel
RESEC("657", false, "S"), // Reduced security based on representative price - Reduzierte Sicherheit
READ("658", false, "S"), // Reduced additional duty based on CIF price - Reduzierter Zusatzzoll
// Serie F - Additional duty on sugar, flour (Zusatzzölle Zucker/Mehl)
AD_SZ("672", false), // Amount of additional duty on sugar - Zusatzzoll Zucker (nicht relevant)
AD_FM("673", false), // Amount of additional duty on flour - Zusatzzoll Mehl (nicht relevant)
AD_SZ("672", false, "F"), // Amount of additional duty on sugar - Zusatzzoll Zucker (nicht relevant)
AD_FM("673", false, "F"), // Amount of additional duty on flour - Zusatzzoll Mehl (nicht relevant)
// Serie E - Levies, export refunds (Abgaben, Ausfuhrerstattungen)
EA("674", false), // Agricultural component - Landwirtschaftliche Komponente (nicht relevant)
EA("674", false, "E"), // Agricultural component - Landwirtschaftliche Komponente (nicht relevant)
// Serie J - Countervailing charge (Ausgleichsabgaben)
TC("690", false), // Countervailing charge - Ausgleichsabgabe
DR("695", true), // Additional duties - Zusatzzölle
SAFDU("696", true); // Additional duties (safeguard) - Schutzmaßnahmen-Zusatzzölle
TC("690", false, "J"), // Countervailing charge - Ausgleichsabgabe
DR("695", true, "J"), // Additional duties - Zusatzzölle
SAFDU("696", true, "J"); // Additional duties (safeguard) - Schutzmaßnahmen-Zusatzzölle
private final String measureCode;
private final boolean containsRelevantDuty;
private final String series;
MeasureType(String measureCode, boolean containsRelevantDuty) {
MeasureType(String measureCode, boolean containsRelevantDuty, String series) {
this.measureCode = measureCode;
this.containsRelevantDuty = containsRelevantDuty;
this.series = series;
}
public String getMeasureCode() {
@ -190,6 +192,10 @@ public enum MeasureType {
return containsRelevantDuty;
}
public String getSeries() {
return series;
}
public static Optional<MeasureType> fromMeasureCode(String measureCode) {
for (MeasureType type : values()) {
if (type.measureCode.equals(measureCode)) {

View file

@ -36,7 +36,7 @@ public class SysErrorRepository {
@Transactional
public Integer insert(SysError error) {
String errorSql = "INSERT INTO sys_error (user_id, title, code, message, pinia, calculation_job_id, bulk_operation_id, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
String errorSql = "INSERT INTO sys_error (user_id, title, code, message, pinia, calculation_job_id, bulk_operation_id, type, request) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
@ -50,6 +50,7 @@ public class SysErrorRepository {
ps.setObject(6, error.getCalculationJobId()); // Use setObject for nullable Integer
ps.setObject(7, error.getBulkOperationId()); // Use setObject for nullable Integer
ps.setString(8, error.getType().name());
ps.setString(9, error.getRequest());
return ps;
}, keyHolder);
@ -68,7 +69,7 @@ public class SysErrorRepository {
@Transactional
public void insert(List<SysError> errors) {
// First insert the sys_error records
String errorSql = "INSERT INTO sys_error (user_id, title, code, message, pinia, calculation_job_id, bulk_operation_id, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
String errorSql = "INSERT INTO sys_error (user_id, title, code, message, pinia, calculation_job_id, bulk_operation_id, type, request) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
@ -83,6 +84,7 @@ public class SysErrorRepository {
ps.setObject(6, error.getCalculationJobId()); // Use setObject for nullable Integer
ps.setObject(7, error.getBulkOperationId()); // Use setObject for nullable Integer
ps.setString(8, error.getType().name());
ps.setString(9, error.getRequest());
return ps;
}, keyHolder);
@ -128,7 +130,7 @@ public class SysErrorRepository {
// Build main query with pagination
String sql = """
SELECT e.id, e.user_id, e.title, e.code, e.message, e.pinia,
e.calculation_job_id, e.bulk_operation_id, e.type, e.created_at
e.calculation_job_id, e.bulk_operation_id, e.type, e.created_at, e.request
FROM sys_error e
""" + whereClause + """
ORDER BY e.created_at DESC
@ -215,6 +217,7 @@ public class SysErrorRepository {
error.setCode(rs.getString("code"));
error.setMessage(rs.getString("message"));
error.setPinia(rs.getString("pinia"));
error.setRequest(rs.getString("request"));
error.setCalculationJobId(rs.getObject("calculation_job_id", Integer.class));
error.setBulkOperationId(rs.getObject("bulk_operation_id", Integer.class));
error.setType(SysErrorType.valueOf(rs.getString("type")));
@ -234,6 +237,7 @@ public class SysErrorRepository {
traceItem.setFile(rs.getString("file"));
traceItem.setMethod(rs.getString("method"));
traceItem.setFullPath(rs.getString("fullPath"));
return traceItem;
}
}

View file

@ -5,8 +5,11 @@ 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.CountryPropertyMappingId;
import de.avatic.lcc.model.db.properties.CustomUnionType;
import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.model.eutaxation.MeasureType;
import de.avatic.lcc.repositories.country.CountryPropertyRepository;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.service.calculation.NomenclatureService;
@ -29,19 +32,34 @@ public class TaxationResolverService {
private final PropertyRepository propertyRepository;
private final ZolltarifnummernApiService zolltarifnummernApiService;
private final NomenclatureService nomenclatureService;
private final CountryPropertyRepository countryPropertyRepository;
public TaxationResolverService(CountryRepository countryRepository, EUTaxationApiService eUTaxationApiService, PropertyRepository propertyRepository, ZolltarifnummernApiService zolltarifnummernApiService, NomenclatureService nomenclatureService) {
public TaxationResolverService(CountryRepository countryRepository, EUTaxationApiService eUTaxationApiService, PropertyRepository propertyRepository, ZolltarifnummernApiService zolltarifnummernApiService, NomenclatureService nomenclatureService, CountryPropertyRepository countryPropertyRepository) {
this.countryRepository = countryRepository;
this.eUTaxationApiService = eUTaxationApiService;
this.propertyRepository = propertyRepository;
this.zolltarifnummernApiService = zolltarifnummernApiService;
this.nomenclatureService = nomenclatureService;
this.countryPropertyRepository = countryPropertyRepository;
}
private Map<TaxationResolverRequest, List<GoodsMeasForWsResponse>> doRequests(List<TaxationResolverRequest> requests) {
private Map<TaxationResolverRequest, TaxationResolverApiResponse> doRequests(List<TaxationResolverRequest> requests) {
var filteredRequests = requests.stream().collect(Collectors.partitioningBy(r -> r.material().getHsCode() != null && r.material().getHsCode().length() < 10));
Map<Integer, CustomUnionType> union = requests.stream()
.map(TaxationResolverRequest::countryId)
.distinct()
.collect(Collectors.toMap(
countryId -> countryId,
countryId -> countryPropertyRepository
.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, countryId)
.map(p -> CustomUnionType.valueOf(p.getCurrentValue()))
.orElseThrow()
));
var byCustomUnion = requests.stream().collect(Collectors.groupingBy(r -> (union.get(r.countryId()))));
var filteredRequests = byCustomUnion.getOrDefault(CustomUnionType.NONE, Collections.emptyList()).stream().collect(Collectors.partitioningBy(r -> r.material().getHsCode() != null && r.material().getHsCode().length() < 10));
var joined = Stream.concat(
filteredRequests.get(false).stream()
@ -49,13 +67,25 @@ public class TaxationResolverService {
.map(r -> new TaxationResolverSingleRequest(r.material().getHsCode(), r.countryId(), r)),
resolveIncompleteHsCodesIntern(filteredRequests.get(true)).stream());
var singleResponses = doSingleRequests(joined.toList());
return requests.stream().collect(Collectors.toMap(
return Stream.of(
byCustomUnion.getOrDefault(CustomUnionType.NONE,Collections.emptyList()).stream().collect(Collectors.toMap(
r -> r,
r -> singleResponses.keySet().stream().filter(k -> k.origin.equals(r)).map(singleResponses::get).toList()
));
r -> new TaxationResolverApiResponse(
r.material(),
true,
singleResponses.keySet().stream().filter(k -> k.origin.equals(r)).map(singleResponses::get).toList()))),
byCustomUnion.getOrDefault(CustomUnionType.EU,Collections.emptyList()).stream().collect(Collectors.toMap(
r -> r,
r -> new TaxationResolverApiResponse(
r.material(),
false,
null))))
.flatMap(map -> map.entrySet().stream()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,(r1,_) -> r1));
}
private List<TaxationResolverSingleRequest> resolveIncompleteHsCodesIntern(List<TaxationResolverRequest> request) {
@ -78,7 +108,6 @@ public class TaxationResolverService {
private Map<TaxationResolverSingleRequest, GoodsMeasForWsResponse> doSingleRequests(List<TaxationResolverSingleRequest> requests) {
Map<Integer, Country> countries = requests.stream()
.map(TaxationResolverSingleRequest::countryId)
.distinct()
@ -87,6 +116,7 @@ public class TaxationResolverService {
countryId -> countryRepository.getById(countryId).orElseThrow()
));
Map<TaxationResolverSingleRequest, CompletableFuture<GoodsMeasForWsResponse>> futureMap =
requests.stream()
.collect(Collectors.toMap(
@ -100,8 +130,7 @@ public class TaxationResolverService {
CompletableFuture.allOf(futureMap.values().toArray(new CompletableFuture[0])).join();
return
futureMap.entrySet().stream()
return futureMap.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().join()
@ -124,19 +153,27 @@ public class TaxationResolverService {
private List<GoodsMeasuresForWsResponse.Measures.Measure> filterToNewestMeasuresPerType(List<GoodsMeasuresForWsResponse.Measures.Measure> measures) {
Map<String, GoodsMeasuresForWsResponse.Measures.Measure> newestByType = new HashMap<>();
ArrayList<GoodsMeasuresForWsResponse.Measures.Measure> unfiltered = new ArrayList<>();
for (GoodsMeasuresForWsResponse.Measures.Measure measure : measures) {
String measureTypeKey = measure.getMeasureType().getMeasureType();
String measureCode = measure.getMeasureType().getMeasureType();
GoodsMeasuresForWsResponse.Measures.Measure existing = newestByType.get(measureTypeKey);
GoodsMeasuresForWsResponse.Measures.Measure existing = newestByType.get(measureCode);
var type = MeasureType.fromMeasureCode(measureCode);
// filter multiples only in C series.
if (type.isPresent() && type.get().getSeries().equals("C")) {
if (existing == null ||
measure.getValidityStartDate().compare(existing.getValidityStartDate()) > 0) {
newestByType.put(measureTypeKey, measure);
newestByType.put(measureCode, measure);
}
} else unfiltered.add(measure);
}
return new ArrayList<>(newestByType.values());
unfiltered.addAll(newestByType.values());
return unfiltered;
}
public boolean validate(String hsCode) {
@ -204,11 +241,18 @@ public class TaxationResolverService {
}
public List<TaxationResolverResponse> getTariffRates(List<TaxationResolverRequest> requests) {
Map<TaxationResolverRequest, List<GoodsMeasForWsResponse>> goodMeasures = doRequests(requests);
var goodMeasures = doRequests(requests);
return goodMeasures.keySet().stream().map(r -> mapToResponse(r, goodMeasures.get(r))).toList();
}
private TaxationResolverResponse mapToResponse(TaxationResolverRequest request, List<GoodsMeasForWsResponse> measForWsResponse) {
private TaxationResolverResponse mapToResponse(TaxationResolverRequest request, TaxationResolverApiResponse apiResponse) {
// source is EU country.
if(!apiResponse.requestExecuted)
return new TaxationResolverResponse(0.0, null, request.material().getHsCode(), request.material(), request.countryId());
List<GoodsMeasForWsResponse> measForWsResponse = apiResponse.apiResponse();
try {
String selectedHsCode = null;
@ -249,7 +293,7 @@ public class TaxationResolverService {
}
}
if (selectedDuty != null && (maxDuty - minDuty < 2)) {
if (selectedDuty != null && (maxDuty - minDuty < 0.02)) {
return new TaxationResolverResponse(selectedDuty, selectedMeasure, selectedHsCode, request.material(), request.countryId());
}
@ -268,6 +312,10 @@ public class TaxationResolverService {
public record TaxationResolverRequest(Material material, Integer countryId) {
}
public record TaxationResolverApiResponse(Material material, boolean requestExecuted,
List<GoodsMeasForWsResponse> apiResponse) {
}
public record TaxationResolverResponse(@Nullable Double tariffRate,
@Nullable String measureCode,
@Nullable String actualHsCode,

View file

@ -40,7 +40,7 @@ public class BulkOperationExecutionService {
try {
execution(id);
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
} catch (Throwable e) {
bulkOperationRepository.updateState(id, BulkOperationState.EXCEPTION);
var error = new SysError();

View file

@ -35,6 +35,7 @@ public class SysErrorTransformer {
entity.setMessage(frontendErrorDTO.getError().getMessage());
entity.setTitle(frontendErrorDTO.getError().getTitle());
entity.setPinia(frontendErrorDTO.getState());
entity.setRequest(frontendErrorDTO.getRequest());
entity.setUserId(userId);
var traceItems = frontendErrorDTO.getError().getTrace();
@ -107,6 +108,7 @@ public class SysErrorTransformer {
dto.setType(sysError.getType().name());
dto.setTrace(sysError.getTrace().stream().map(this::toSysErrorTraceItemDto).toList());
dto.setCreatedAt(sysError.getCreatedAt());
dto.setRequest(sysError.getRequest());
return dto;
}

View file

@ -637,6 +637,7 @@ CREATE TABLE IF NOT EXISTS sys_error
title VARCHAR(255) NOT NULL,
code VARCHAR(255) NOT NULL,
message VARCHAR(1024) NOT NULL,
request TEXT,
pinia TEXT,
calculation_job_id INT DEFAULT NULL,
bulk_operation_id INT DEFAULT NULL,

View file

@ -637,6 +637,7 @@ CREATE TABLE IF NOT EXISTS sys_error
title VARCHAR(255) NOT NULL,
code VARCHAR(255) NOT NULL,
message VARCHAR(1024) NOT NULL,
request TEXT,
pinia TEXT,
calculation_job_id INT DEFAULT NULL,
bulk_operation_id INT DEFAULT NULL,