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

View file

@ -1,5 +1,6 @@
import logger from "@/logger.js"; import logger from "@/logger.js";
import {useErrorStore} from "@/store/error.js"; import {useErrorStore} from "@/store/error.js";
import {config} from "@/config";
const getCsrfToken = () => { const getCsrfToken = () => {
const value = `; ${document.cookie}`; const value = `; ${document.cookie}`;
@ -13,44 +14,32 @@ const getCsrfToken = () => {
let sessionRefreshInterval = null; let sessionRefreshInterval = null;
let lastActivity = Date.now(); let lastActivity = Date.now();
const refreshSession = async () => { const refreshSession = async () => {
try { try {
// Ein simpler authenticated GET request hält die Session am Leben await performRequest(null, 'GET', `${config.backendUrl}/error/`, null);
await fetch('/api/session/keepalive', { logger.log('Session refreshed');
method: 'GET',
credentials: 'include'
});
console.log('Session refreshed');
} catch (e) { } catch (e) {
console.error('Session refresh failed', e); logger.error('Session refresh failed', e);
} }
} }
const trackActivity = () => { const trackActivity = () => {
lastActivity = Date.now(); lastActivity = Date.now();
} }
const startSessionRefresh = () => { const startSessionRefresh = () => {
['mousedown', 'keypress', 'scroll', 'touchstart'].forEach(event => {
document.addEventListener(event, trackActivity);
});
if (sessionRefreshInterval) { if (sessionRefreshInterval) {
clearInterval(sessionRefreshInterval); clearInterval(sessionRefreshInterval);
} }
sessionRefreshInterval = setInterval(() => { sessionRefreshInterval = setInterval(async () => {
const timeSinceActivity = Date.now() - lastActivity; const timeSinceActivity = Date.now() - lastActivity;
if (timeSinceActivity < (12 * 60 * 60 * 1000)) { if (timeSinceActivity < (10 * 60 * 1000)) {
refreshSession(); await refreshSession();
} }
}, 10 * 60 * 1000); }, 2 * 60 * 1000);
} }
const stopSessionRefresh = () => { 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); logger.info("Request:", request);
const resp = await executeRequest(null, request); const resp = await executeRequest(null, request);
const downloadUrl = window.URL.createObjectURL(resp.data); const downloadUrl = window.URL.createObjectURL(resp.data);
// Create temporary link element and trigger download // Create temporary link element and trigger download
@ -147,17 +141,26 @@ function handleErrorResponse(data, requestingStore, request) {
const error = new Error('Internal backend error'); const error = new Error('Internal backend error');
error.errorObj = errorObj; 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)) { 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); logger.error(errorObj, request.expectedException);
const errorStore = useErrorStore();
void errorStore.addError(errorObj, {store: requestingStore, request: request}); 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; throw error;
} }
const executeRequest = async (requestingStore, request) => { const executeRequest = async (requestingStore, request) => {
trackActivity();
const response = await fetch(request.url, request.params const response = await fetch(request.url, request.params
).catch(e => { ).catch(e => {
const error = { const error = {
@ -180,7 +183,7 @@ const executeRequest = async (requestingStore, request) => {
} }
let data = null; let data = null;
if (request.expectResponse) { if (request.expectResponse) { // backend should return something
try { try {
if (request.type === 'blob' && response.ok) { if (request.type === 'blob' && response.ok) {
data = await response.blob(); data = await response.blob();
@ -207,7 +210,7 @@ const executeRequest = async (requestingStore, request) => {
} else { } else {
if (!response.ok) { if (!response.ok) {
const data = await response.json().catch(e => { const data = await response.json().catch(_ => {
const error = { const error = {
code: "Return code error " + response.status, code: "Return code error " + response.status,
message: "Server returned wrong response code", 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> <style scoped>
.flag-container img {
filter: saturate(0.9) brightness(0.95) hue-rotate(-10deg);
}
.flag-container { .flag-container {
display: flex; display: flex;
align-items: center; 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++, id: this.nextId++,
message: options.message || 'Notification', message: options.message || 'Notification',
title: options.title || null, title: options.title || null,
variant: options.variant || 'primary', variant: this.mapVariant(options.variant || 'primary' ),
duration: options.duration !== undefined ? options.duration : 5000, duration: options.duration !== undefined ? options.duration : 5000,
icon: options.icon ? `Ph${options.icon.charAt(0).toUpperCase() + options.icon.slice(1)}` : null, icon: options.icon ? `Ph${options.icon.charAt(0).toUpperCase() + options.icon.slice(1)}` : null,
}; };
@ -72,6 +72,21 @@ export default {
return toast.id 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 * Remove a toast by ID
* @param {number} id - Toast ID * @param {number} id - Toast ID
@ -189,8 +204,8 @@ export default {
} }
.toast--secondary { .toast--secondary {
background-color: #002F54; color: #002F54;
color: #ffffff; background-color: #c3cfdf;
} }
.toast--exception{ .toast--exception{

View file

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

View file

@ -23,12 +23,12 @@
</div> </div>
<div v-if="tariffUnlocked" class="tariff-rate-info-header"> <div v-if="tariffUnlocked" class="tariff-rate-info-container">
<ph-warning size="24" /> <ph-warning size="24" />
<div> <div>
<div>Tariff rate lookup failed</div> <div>Automatic tariff rate determination was ambiguous</div>
<div class="tariff-rate-info-headerbox"> <div class="tariff-rate-info-text">
Please contact a customs expert to obtain HS code and tariff rate. Please contact a customs expert to obtain correct HS code and tariff rate.
</div> </div>
</div> </div>
</div> </div>
@ -179,12 +179,14 @@ export default {
const multiple = 10 ** digits; const multiple = 10 ** digits;
return Math.round(number * multiple) / multiple; return Math.round(number * multiple) / multiple;
}, },
hsCodeChanged(event) { hsCodeChanged(event) {
const sanitized = event.target.value let sanitized = event.target.value
.replace(/\D/g, '') .replace(/\D/g, '')
.substring(0, 10); .substring(0, 10);
if(sanitized.length !== 10)
sanitized = null;
this.$emit("update:hsCode", sanitized); this.$emit("update:hsCode", sanitized);
event.target.value = sanitized; event.target.value = sanitized;
} }
@ -305,13 +307,13 @@ export default {
position: relative; position: relative;
} }
.tariff-rate-info-headerbox { .tariff-rate-info-text {
color: #002F54; color: #002F54;
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 400; font-weight: 400;
} }
.tariff-rate-info-header { .tariff-rate-info-container {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.8rem; 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 IconButton from "@/components/UI/IconButton.vue";
import ErrorModalOverview from "@/components/layout/error/ErrorModalOverview.vue"; import ErrorModalOverview from "@/components/layout/error/ErrorModalOverview.vue";
import ErrorModalPiniaStore from "@/components/layout/error/ErrorModalPiniaStore.vue"; import ErrorModalPiniaStore from "@/components/layout/error/ErrorModalPiniaStore.vue";
import ErrorModalRequest from "@/components/layout/error/ErrorModalRequest.vue";
export default { export default {
name: "ErrorModal", name: "ErrorModal",
@ -37,24 +38,38 @@ export default {
}, },
computed: { computed: {
tabsError() { tabsError() {
return [ const tabs = [];
{
title: 'Overview', tabs.push({
component: markRaw(ErrorModalOverview), 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}, props: {isSelected: false, error: this.error},
}, });
{
if (this.error.trace)
tabs.push({
title: 'Stack trace', title: 'Stack trace',
component: markRaw(ErrorModalTraceView), component: markRaw(ErrorModalTraceView),
props: {isSelected: false, error: this.error}, props: {isSelected: false, error: this.error},
}, });
{
if (this.error.pinia)
tabs.push({
title: 'Frontend storage', title: 'Frontend storage',
component: markRaw(ErrorModalPiniaStore), component: markRaw(ErrorModalPiniaStore),
props: {isSelected: false, error: this.error}, props: {isSelected: false, error: this.error},
}, });
]
return tabs;
} }
}, },
} }

View file

@ -4,7 +4,6 @@
<div> <div>
<basic-button icon="clipboard" @click="copyToClipboard">Copy error to clipboard</basic-button> <basic-button icon="clipboard" @click="copyToClipboard">Copy error to clipboard</basic-button>
<Toast ref="toast"/>
</div> </div>
<div class="error-section"> <div class="error-section">
@ -87,12 +86,13 @@
import BasicBadge from "@/components/UI/BasicBadge.vue"; import BasicBadge from "@/components/UI/BasicBadge.vue";
import {buildDate} from "@/common.js"; import {buildDate} from "@/common.js";
import BasicButton from "@/components/UI/BasicButton.vue"; import BasicButton from "@/components/UI/BasicButton.vue";
import Toast from "@/components/UI/Toast.vue"; import logger from "@/logger.js";
import {error} from "loglevel"; import {mapStores} from "pinia";
import {useErrorStore} from "@/store/error.js";
export default { export default {
name: "ErrorModalOverview", name: "ErrorModalOverview",
components: {Toast, BasicButton, BasicBadge}, components: {BasicButton, BasicBadge},
props: { props: {
isSelected: { isSelected: {
type: Boolean, type: Boolean,
@ -108,20 +108,31 @@ export default {
try { try {
await navigator.clipboard.writeText(JSON.stringify(this.error)); await navigator.clipboard.writeText(JSON.stringify(this.error));
this.$refs.toast.addToast({ this.errorStore.addNotification({
icon: 'Clipboard', icon: 'Clipboard',
message: "The error has been copied to clipboard", message: "The error has been copied to clipboard",
title: "Successful copied to clipboard", title: "Successful copied to clipboard",
variant: 'success', variant: 'success',
duration: 4000 duration: 4000
}) }
)
} catch (err) { } 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: { computed: {
...mapStores(useErrorStore),
badgeIcon() { badgeIcon() {
if (this.error.type === "FRONTEND") { if (this.error.type === "FRONTEND") {
return "desktop"; return "desktop";
@ -214,6 +225,7 @@ code {
.error-label { .error-label {
font-weight: 500; font-weight: 500;
font-size: 1.4rem; font-size: 1.4rem;
align-self: start;
color: #6B869C; color: #6B869C;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05rem; letter-spacing: 0.05rem;
@ -225,21 +237,8 @@ code {
word-break: break-word; word-break: break-word;
} }
.error-title { .error-section:last-child {
font-weight: 500; border-bottom: none;
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-code code { .error-code code {
@ -247,46 +246,4 @@ code {
font-size: 1.3rem; 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> </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 class="text-container"><input class="input-field" v-model="nodeAddress" @input="checkChange"/></div>
</div> </div>
<div> <div>
<basic-button icon="SealCheck" @click.prevent="verifyAddress" :disabled="addressVerified" @submit.prevent>Check address</basic-button> <basic-button icon="SealCheck" @click.prevent="verifyAddress" :disabled="addressVerified" @submit.prevent>
<Toast ref="toast"/> Check address
</basic-button>
</div> </div>
<div class="input-field-caption" v-if="addressVerified">Country:</div> <div class="input-field-caption" v-if="addressVerified">Country:</div>
<div class="country-field" v-if="addressVerified"> <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> 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="create-new-node-footer"> <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> <basic-button @click.prevent="cancel" variant="secondary" :show-icon="false">Cancel</basic-button>
</div> </div>
</form> </form>
@ -43,12 +45,11 @@ import BasicButton from "@/components/UI/BasicButton.vue";
import {mapStores} from "pinia"; import {mapStores} from "pinia";
import {useNodeStore} from "@/store/node.js"; import {useNodeStore} from "@/store/node.js";
import OpenStreetMapEmbed from "@/components/UI/OpenStreetMapEmbed.vue"; import OpenStreetMapEmbed from "@/components/UI/OpenStreetMapEmbed.vue";
import Toast from "@/components/UI/Toast.vue";
import Flag from "@/components/UI/Flag.vue"; import Flag from "@/components/UI/Flag.vue";
export default { export default {
name: "CreateNewNode", name: "CreateNewNode",
components: {Flag, Toast, OpenStreetMapEmbed, BasicButton}, components: {Flag, OpenStreetMapEmbed, BasicButton},
emits: ['created', 'close'], emits: ['created', 'close'],
data() { data() {
return { return {
@ -114,15 +115,7 @@ export default {
this.addressVerified = false; this.addressVerified = false;
this.verifiedAddress = null; this.verifiedAddress = null;
if (error !== null) { if (error === null && node !== null) {
this.$refs.toast.addToast({
icon: 'warning',
message: error.message,
title: "Cannot locate address.",
variant: 'exception',
duration: 8000
})
} else if (node) {
this.nodeCoordinates = node.location; this.nodeCoordinates = node.location;
this.nodeAddress = node.address; this.nodeAddress = node.address;
@ -132,7 +125,6 @@ export default {
this.addressVerified = true; this.addressVerified = true;
this.verifiedAddress = node.address; this.verifiedAddress = node.address;
this.node = node; this.node = node;
} else {
} }
}, },

View file

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

View file

@ -17,7 +17,6 @@
</div> </div>
</div> </div>
<Toast ref="toast"/>
<div v-if="premiseSingleEditStore.showLoadingSpinner" 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">
@ -112,14 +111,14 @@ import NotificationBar from "@/components/UI/NotificationBar.vue";
import Modal from "@/components/UI/Modal.vue"; import Modal from "@/components/UI/Modal.vue";
import TraceView from "@/components/layout/TraceView.vue"; 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 {UrlSafeBase64} from "@/common.js"; import {UrlSafeBase64} from "@/common.js";
import {usePremiseSingleEditStore} from "@/store/premiseSingleEdit.js"; import {usePremiseSingleEditStore} from "@/store/premiseSingleEdit.js";
import logger from "@/logger.js";
export default { export default {
name: "SingleEdit", name: "SingleEdit",
components: { components: {
Toast,
IconButton, IconButton,
TraceView, TraceView,
Modal, Modal,
@ -157,7 +156,6 @@ export default {
return "Please wait. Routing ..." return "Please wait. Routing ..."
return "Please wait. Calculating ..."; return "Please wait. Calculating ...";
} }
}, },
methods: { methods: {
@ -166,28 +164,7 @@ export default {
this.showCalculationModal = true; this.showCalculationModal = true;
const error = await this.premiseSingleEditStore.startCalculation(); const error = await this.premiseSingleEditStore.startCalculation();
if (error !== null) { 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 {
this.close(); this.close();
} }
@ -204,25 +181,13 @@ export default {
} }
}, },
async save(type) { async save(type) {
let success = false;
if (type === 'price') { if (type === 'price') {
success = await this.premiseSingleEditStore.savePrice(); await this.premiseSingleEditStore.savePrice();
} else if (type === 'material') { } else if (type === 'material') {
success = await this.premiseSingleEditStore.saveMaterial(); await this.premiseSingleEditStore.saveMaterial();
} else if (type === 'packaging') { } 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() { trace() {
this.traceModal = true; this.traceModal = true;
@ -234,7 +199,8 @@ export default {
if (this.$route.params.ids) if (this.$route.params.ids)
this.bulkEditQuery = this.$route.params.ids; this.bulkEditQuery = this.$route.params.ids;
this.premiseSingleEditStore.load(this.id) if (!isNaN(this.id))
this.premiseSingleEditStore.load(this.id)
}, },
@ -305,11 +271,6 @@ export default {
} }
} }
.trace-link {
cursor: pointer;
text-decoration: underline;
}
.space-around { .space-around {
margin: 3rem; margin: 3rem;
} }
@ -333,7 +294,4 @@ export default {
align-items: center; align-items: center;
} }
.destination-list-container {
margin: 2.4rem 1.6rem;
}
</style> </style>

View file

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

View file

@ -35,7 +35,6 @@ export const useNodeStore = defineStore('node', {
let error = null; let error = null;
params.append('address', address); 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 => { 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; error = e.errorObj;
}); });
return {node: data?.data, error: error}; return {node: data?.data, error: error};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -36,7 +36,7 @@ public class SysErrorRepository {
@Transactional @Transactional
public Integer insert(SysError error) { 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(); KeyHolder keyHolder = new GeneratedKeyHolder();
@ -50,6 +50,7 @@ public class SysErrorRepository {
ps.setObject(6, error.getCalculationJobId()); // Use setObject for nullable Integer ps.setObject(6, error.getCalculationJobId()); // Use setObject for nullable Integer
ps.setObject(7, error.getBulkOperationId()); // Use setObject for nullable Integer ps.setObject(7, error.getBulkOperationId()); // Use setObject for nullable Integer
ps.setString(8, error.getType().name()); ps.setString(8, error.getType().name());
ps.setString(9, error.getRequest());
return ps; return ps;
}, keyHolder); }, keyHolder);
@ -68,7 +69,7 @@ public class SysErrorRepository {
@Transactional @Transactional
public void insert(List<SysError> errors) { public void insert(List<SysError> errors) {
// First insert the sys_error records // 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(); KeyHolder keyHolder = new GeneratedKeyHolder();
@ -83,6 +84,7 @@ public class SysErrorRepository {
ps.setObject(6, error.getCalculationJobId()); // Use setObject for nullable Integer ps.setObject(6, error.getCalculationJobId()); // Use setObject for nullable Integer
ps.setObject(7, error.getBulkOperationId()); // Use setObject for nullable Integer ps.setObject(7, error.getBulkOperationId()); // Use setObject for nullable Integer
ps.setString(8, error.getType().name()); ps.setString(8, error.getType().name());
ps.setString(9, error.getRequest());
return ps; return ps;
}, keyHolder); }, keyHolder);
@ -128,7 +130,7 @@ public class SysErrorRepository {
// Build main query with pagination // Build main query with pagination
String sql = """ String sql = """
SELECT e.id, e.user_id, e.title, e.code, e.message, e.pinia, 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 FROM sys_error e
""" + whereClause + """ """ + whereClause + """
ORDER BY e.created_at DESC ORDER BY e.created_at DESC
@ -215,6 +217,7 @@ public class SysErrorRepository {
error.setCode(rs.getString("code")); error.setCode(rs.getString("code"));
error.setMessage(rs.getString("message")); error.setMessage(rs.getString("message"));
error.setPinia(rs.getString("pinia")); error.setPinia(rs.getString("pinia"));
error.setRequest(rs.getString("request"));
error.setCalculationJobId(rs.getObject("calculation_job_id", Integer.class)); error.setCalculationJobId(rs.getObject("calculation_job_id", Integer.class));
error.setBulkOperationId(rs.getObject("bulk_operation_id", Integer.class)); error.setBulkOperationId(rs.getObject("bulk_operation_id", Integer.class));
error.setType(SysErrorType.valueOf(rs.getString("type"))); error.setType(SysErrorType.valueOf(rs.getString("type")));
@ -234,6 +237,7 @@ public class SysErrorRepository {
traceItem.setFile(rs.getString("file")); traceItem.setFile(rs.getString("file"));
traceItem.setMethod(rs.getString("method")); traceItem.setMethod(rs.getString("method"));
traceItem.setFullPath(rs.getString("fullPath")); traceItem.setFullPath(rs.getString("fullPath"));
return traceItem; 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.dto.generic.PropertyDTO;
import de.avatic.lcc.model.db.country.Country; import de.avatic.lcc.model.db.country.Country;
import de.avatic.lcc.model.db.materials.Material; 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.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.model.eutaxation.MeasureType; 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.country.CountryRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository; import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.service.calculation.NomenclatureService; import de.avatic.lcc.service.calculation.NomenclatureService;
@ -29,19 +32,34 @@ public class TaxationResolverService {
private final PropertyRepository propertyRepository; private final PropertyRepository propertyRepository;
private final ZolltarifnummernApiService zolltarifnummernApiService; private final ZolltarifnummernApiService zolltarifnummernApiService;
private final NomenclatureService nomenclatureService; 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.countryRepository = countryRepository;
this.eUTaxationApiService = eUTaxationApiService; this.eUTaxationApiService = eUTaxationApiService;
this.propertyRepository = propertyRepository; this.propertyRepository = propertyRepository;
this.zolltarifnummernApiService = zolltarifnummernApiService; this.zolltarifnummernApiService = zolltarifnummernApiService;
this.nomenclatureService = nomenclatureService; 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( var joined = Stream.concat(
filteredRequests.get(false).stream() filteredRequests.get(false).stream()
@ -49,13 +67,25 @@ public class TaxationResolverService {
.map(r -> new TaxationResolverSingleRequest(r.material().getHsCode(), r.countryId(), r)), .map(r -> new TaxationResolverSingleRequest(r.material().getHsCode(), r.countryId(), r)),
resolveIncompleteHsCodesIntern(filteredRequests.get(true)).stream()); resolveIncompleteHsCodesIntern(filteredRequests.get(true)).stream());
var singleResponses = doSingleRequests(joined.toList()); var singleResponses = doSingleRequests(joined.toList());
return requests.stream().collect(Collectors.toMap( return Stream.of(
r -> r, byCustomUnion.getOrDefault(CustomUnionType.NONE,Collections.emptyList()).stream().collect(Collectors.toMap(
r -> singleResponses.keySet().stream().filter(k -> k.origin.equals(r)).map(singleResponses::get).toList() r -> r,
)); 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) { private List<TaxationResolverSingleRequest> resolveIncompleteHsCodesIntern(List<TaxationResolverRequest> request) {
@ -78,7 +108,6 @@ public class TaxationResolverService {
private Map<TaxationResolverSingleRequest, GoodsMeasForWsResponse> doSingleRequests(List<TaxationResolverSingleRequest> requests) { private Map<TaxationResolverSingleRequest, GoodsMeasForWsResponse> doSingleRequests(List<TaxationResolverSingleRequest> requests) {
Map<Integer, Country> countries = requests.stream() Map<Integer, Country> countries = requests.stream()
.map(TaxationResolverSingleRequest::countryId) .map(TaxationResolverSingleRequest::countryId)
.distinct() .distinct()
@ -87,6 +116,7 @@ public class TaxationResolverService {
countryId -> countryRepository.getById(countryId).orElseThrow() countryId -> countryRepository.getById(countryId).orElseThrow()
)); ));
Map<TaxationResolverSingleRequest, CompletableFuture<GoodsMeasForWsResponse>> futureMap = Map<TaxationResolverSingleRequest, CompletableFuture<GoodsMeasForWsResponse>> futureMap =
requests.stream() requests.stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
@ -100,12 +130,11 @@ public class TaxationResolverService {
CompletableFuture.allOf(futureMap.values().toArray(new CompletableFuture[0])).join(); CompletableFuture.allOf(futureMap.values().toArray(new CompletableFuture[0])).join();
return return futureMap.entrySet().stream()
futureMap.entrySet().stream() .collect(Collectors.toMap(
.collect(Collectors.toMap( Map.Entry::getKey,
Map.Entry::getKey, entry -> entry.getValue().join()
entry -> entry.getValue().join() ));
));
} }
private Optional<Double> extractDuty(GoodsMeasuresForWsResponse.Measures.Measure measure) { private Optional<Double> extractDuty(GoodsMeasuresForWsResponse.Measures.Measure measure) {
@ -124,19 +153,27 @@ public class TaxationResolverService {
private List<GoodsMeasuresForWsResponse.Measures.Measure> filterToNewestMeasuresPerType(List<GoodsMeasuresForWsResponse.Measures.Measure> measures) { private List<GoodsMeasuresForWsResponse.Measures.Measure> filterToNewestMeasuresPerType(List<GoodsMeasuresForWsResponse.Measures.Measure> measures) {
Map<String, GoodsMeasuresForWsResponse.Measures.Measure> newestByType = new HashMap<>(); Map<String, GoodsMeasuresForWsResponse.Measures.Measure> newestByType = new HashMap<>();
ArrayList<GoodsMeasuresForWsResponse.Measures.Measure> unfiltered = new ArrayList<>();
for (GoodsMeasuresForWsResponse.Measures.Measure measure : measures) { 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(measureCode, measure);
}
} else unfiltered.add(measure);
if (existing == null ||
measure.getValidityStartDate().compare(existing.getValidityStartDate()) > 0) {
newestByType.put(measureTypeKey, measure);
}
} }
return new ArrayList<>(newestByType.values()); unfiltered.addAll(newestByType.values());
return unfiltered;
} }
public boolean validate(String hsCode) { public boolean validate(String hsCode) {
@ -204,11 +241,18 @@ public class TaxationResolverService {
} }
public List<TaxationResolverResponse> getTariffRates(List<TaxationResolverRequest> requests) { 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(); 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 { try {
String selectedHsCode = null; 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()); 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 TaxationResolverRequest(Material material, Integer countryId) {
} }
public record TaxationResolverApiResponse(Material material, boolean requestExecuted,
List<GoodsMeasForWsResponse> apiResponse) {
}
public record TaxationResolverResponse(@Nullable Double tariffRate, public record TaxationResolverResponse(@Nullable Double tariffRate,
@Nullable String measureCode, @Nullable String measureCode,
@Nullable String actualHsCode, @Nullable String actualHsCode,

View file

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

View file

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

View file

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