Merge pull request 'dev' (#23) from dev into main

Reviewed-on: #23
This commit is contained in:
Seazn 2026-05-21 17:34:47 +00:00
commit b5918e091a
52 changed files with 5637 additions and 362 deletions

View File

@ -12,16 +12,61 @@ function buildPictureUrlFromKey(key) {
return key; // fallback: store key only
}
function normalizePictureFiles(req) {
const files = [];
// upload.any() -> req.files is usually an array
if (Array.isArray(req.files)) {
files.push(...req.files);
}
// Defensive fallback for potential object-shaped middleware output
if (req.files && !Array.isArray(req.files) && typeof req.files === 'object') {
for (const value of Object.values(req.files)) {
if (Array.isArray(value)) files.push(...value);
else if (value) files.push(value);
}
}
if (req.file) files.push(req.file);
return files.filter(Boolean);
}
function toCoffeeResponse(row) {
const pictures = Array.isArray(row?.pictures) && row.pictures.length
? row.pictures.map((p) => ({
...p,
url: p.object_storage_id ? buildPictureUrlFromKey(p.object_storage_id) : ''
}))
: (row?.object_storage_id
? [{
id: null,
coffee_id: row.id,
object_storage_id: row.object_storage_id,
original_filename: row.original_filename || null,
sort_order: 0,
created_at: row.created_at || null,
url: buildPictureUrlFromKey(row.object_storage_id)
}]
: []);
const pictureUrls = pictures.map((p) => p.url).filter(Boolean);
return {
...row,
pictureUrl: pictureUrls[0] || '',
pictureUrls,
pictures,
};
}
exports.list = async (req, res) => {
const rows = await CoffeeService.list();
const items = (rows || []).map(r => ({
...r,
pictureUrl: r.object_storage_id ? buildPictureUrlFromKey(r.object_storage_id) : ''
}));
const items = (rows || []).map((r) => toCoffeeResponse(r));
res.json(items);
};
exports.create = async (req, res) => {
let uploadedKeys = [];
try {
const { title, description, price } = req.body;
const currency = req.body.currency || 'EUR';
@ -31,19 +76,23 @@ exports.create = async (req, res) => {
const interval_count = 1;
const state = req.body.state === 'false' || req.body.state === false ? false : true; // default available
// If file uploaded, push to Exoscale and set object_storage_id
// If files uploaded, push to Exoscale and store all uploaded images
let object_storage_id = null;
let original_filename = null;
let uploadedKey = null;
if (req.file) {
let images = [];
const incomingFiles = normalizePictureFiles(req);
logger.info('[CoffeeController.create] incoming multipart files', {
count: incomingFiles.length,
fields: incomingFiles.map((f) => f?.fieldname).filter(Boolean),
});
if (incomingFiles.length > 10) {
return res.status(400).json({ error: 'Maximum 10 images allowed' });
}
if (incomingFiles.length) {
const allowedMime = ['image/jpeg','image/png','image/webp'];
if (!allowedMime.includes(req.file.mimetype)) {
return res.status(400).json({ error: 'Invalid image type. Allowed: JPG, PNG, WebP' });
}
const maxBytes = 10 * 1024 * 1024; // 10MB
if (req.file.size > maxBytes) {
return res.status(400).json({ error: 'Image exceeds 10MB limit' });
}
const s3 = new S3Client({
region: process.env.EXOSCALE_REGION,
endpoint: process.env.EXOSCALE_ENDPOINT,
@ -52,18 +101,38 @@ exports.create = async (req, res) => {
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
},
});
const key = `coffee/products/${Date.now()}_${req.file.originalname}`;
await s3.send(new PutObjectCommand({
Bucket: process.env.EXOSCALE_BUCKET,
Key: key,
Body: req.file.buffer,
ContentType: req.file.mimetype,
ACL: 'public-read'
}));
object_storage_id = key;
original_filename = req.file.originalname;
uploadedKey = key;
logger.info('[CoffeeController.create] uploaded picture', { key });
for (let i = 0; i < incomingFiles.length; i += 1) {
const file = incomingFiles[i];
if (!allowedMime.includes(file.mimetype)) {
return res.status(400).json({ error: 'Invalid image type. Allowed: JPG, PNG, WebP' });
}
if (file.size > maxBytes) {
return res.status(400).json({ error: 'Image exceeds 10MB limit' });
}
const key = `coffee/products/${Date.now()}_${i}_${file.originalname}`;
await s3.send(new PutObjectCommand({
Bucket: process.env.EXOSCALE_BUCKET,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
ACL: 'public-read'
}));
uploadedKeys.push(key);
images.push({
object_storage_id: key,
original_filename: file.originalname,
sort_order: i,
});
}
if (images[0]) {
object_storage_id = images[0].object_storage_id;
original_filename = images[0].original_filename;
}
logger.info('[CoffeeController.create] uploaded pictures', { count: images.length });
}
const created = await CoffeeService.create({
@ -76,17 +145,15 @@ exports.create = async (req, res) => {
interval_count,
object_storage_id,
original_filename,
images,
state,
});
res.status(201).json({
...created,
pictureUrl: created.object_storage_id ? buildPictureUrlFromKey(created.object_storage_id) : ''
});
res.status(201).json(toCoffeeResponse(created));
} catch (e) {
logger.error('[CoffeeController.create] error', { msg: e.message, stack: e.stack?.split('\n')[0] });
// best-effort cleanup of uploaded object on failure
try {
if (object_storage_id) {
if (uploadedKeys.length) {
const s3 = new S3Client({
region: process.env.EXOSCALE_REGION,
endpoint: process.env.EXOSCALE_ENDPOINT,
@ -95,7 +162,9 @@ exports.create = async (req, res) => {
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
},
});
await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: object_storage_id }));
for (const key of uploadedKeys) {
await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: key }));
}
}
} catch (cleanupErr) {
logger.warn('[CoffeeController.create] cleanup failed', { msg: cleanupErr.message });
@ -251,13 +320,181 @@ exports.remove = async (req, res) => {
exports.listActive = async (req, res) => {
try {
const rows = await CoffeeService.listActive();
const items = (rows || []).map(r => ({
...r,
pictureUrl: r.object_storage_id ? buildPictureUrlFromKey(r.object_storage_id) : ''
}));
const items = (rows || []).map((r) => toCoffeeResponse(r));
res.json(items);
} catch (e) {
logger.error('[CoffeeController.listActive] error', { msg: e.message });
res.status(500).json({ error: 'Failed to fetch active coffee products' });
}
};
exports.getPictures = async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
if (!Number.isFinite(id) || id <= 0) {
return res.status(400).json({ error: 'Invalid id' });
}
const row = await CoffeeService.get(id);
if (!row) {
return res.status(404).json({ error: 'Not found' });
}
const normalized = toCoffeeResponse(row);
return res.json({
coffeeId: id,
pictures: normalized.pictures,
pictureUrls: normalized.pictureUrls,
pictureUrl: normalized.pictureUrl,
});
} catch (e) {
logger.error('[CoffeeController.getPictures] error', { msg: e.message });
return res.status(500).json({ error: 'Failed to fetch coffee pictures' });
}
};
exports.editPictures = async (req, res) => {
let uploadedKeys = [];
try {
const id = parseInt(req.params.id, 10);
if (!Number.isFinite(id) || id <= 0) {
return res.status(400).json({ error: 'Invalid id' });
}
const parseBoolean = (value, fallback = false) => {
if (value === undefined || value === null || value === '') return fallback;
if (typeof value === 'boolean') return value;
const normalized = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return fallback;
};
const parseIds = (value) => {
if (value === undefined || value === null || value === '') return [];
if (Array.isArray(value)) return value.map((x) => Number(x)).filter((x) => Number.isFinite(x) && x > 0);
const text = String(value).trim();
if (!text) return [];
try {
const parsed = JSON.parse(text);
if (Array.isArray(parsed)) {
return parsed.map((x) => Number(x)).filter((x) => Number.isFinite(x) && x > 0);
}
} catch (_) {
// Fall through to CSV parsing.
}
return text
.split(',')
.map((x) => Number(String(x).trim()))
.filter((x) => Number.isFinite(x) && x > 0);
};
const replaceAll = parseBoolean(req.body?.replaceAll, false);
const removePictureIds = parseIds(req.body?.removePictureIds);
const incomingFiles = normalizePictureFiles(req);
if (incomingFiles.length > 10) {
return res.status(400).json({ error: 'Maximum 10 images allowed per edit request' });
}
const images = [];
if (incomingFiles.length) {
const allowedMime = ['image/jpeg','image/png','image/webp'];
const maxBytes = 10 * 1024 * 1024; // 10MB
const s3 = new S3Client({
region: process.env.EXOSCALE_REGION,
endpoint: process.env.EXOSCALE_ENDPOINT,
credentials: {
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
},
});
for (let i = 0; i < incomingFiles.length; i += 1) {
const file = incomingFiles[i];
if (!allowedMime.includes(file.mimetype)) {
return res.status(400).json({ error: 'Invalid image type. Allowed: JPG, PNG, WebP' });
}
if (file.size > maxBytes) {
return res.status(400).json({ error: 'Image exceeds 10MB limit' });
}
const key = `coffee/products/${Date.now()}_${i}_${file.originalname}`;
await s3.send(new PutObjectCommand({
Bucket: process.env.EXOSCALE_BUCKET,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
ACL: 'public-read'
}));
uploadedKeys.push(key);
images.push({
object_storage_id: key,
original_filename: file.originalname,
});
}
}
const result = await CoffeeService.editPictures(id, {
replaceAll,
removePictureIds,
images,
});
if (!result || !result.updated) {
return res.status(404).json({ error: 'Not found' });
}
const deletedKeys = (result.deleted || [])
.map((x) => x?.object_storage_id)
.filter(Boolean);
if (deletedKeys.length) {
try {
const s3 = new S3Client({
region: process.env.EXOSCALE_REGION,
endpoint: process.env.EXOSCALE_ENDPOINT,
credentials: {
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
},
});
for (const key of deletedKeys) {
await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: key }));
}
} catch (cleanupErr) {
logger.warn('[CoffeeController.editPictures] cleanup failed', { msg: cleanupErr.message });
}
}
return res.json(toCoffeeResponse(result.updated));
} catch (e) {
logger.error('[CoffeeController.editPictures] error', { msg: e.message });
try {
if (uploadedKeys.length) {
const s3 = new S3Client({
region: process.env.EXOSCALE_REGION,
endpoint: process.env.EXOSCALE_ENDPOINT,
credentials: {
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
},
});
for (const key of uploadedKeys) {
await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: key }));
}
}
} catch (cleanupErr) {
logger.warn('[CoffeeController.editPictures] rollback cleanup failed', { msg: cleanupErr.message });
}
return res.status(500).json({ error: 'Failed to edit coffee pictures' });
}
};

View File

@ -1,111 +1,62 @@
const CompanySettingsRepository = require('../../repositories/settings/CompanySettingsRepository');
const { logger } = require('../../middleware/logger');
const crypto = require('crypto');
const repo = new CompanySettingsRepository();
const ALLOWED_LOGO_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/svg+xml']);
const MAX_LOGO_BYTES = 1024 * 1024;
class CompanySettingsController {
static _normalizeBase64ImageInput(value) {
if (value === undefined) return undefined;
if (value === null) return null;
const stripDataUri = (str) => {
const s = String(str || '').trim();
if (!s) return '';
const m = s.match(/^data:([^;]+);base64,(.*)$/i);
return m ? (m[2] || '').trim() : s;
};
if (typeof value === 'string') {
// If base64 came via urlencoded forms, '+' often becomes ' ' after decoding.
// Restoring spaces back to '+' makes the payload usable again.
let normalized = stripDataUri(value);
if (normalized.includes(' ') && !normalized.includes('+')) {
normalized = normalized.replace(/ /g, '+');
}
normalized = normalized.replace(/\s+/g, '');
return normalized || null;
}
if (typeof value === 'object') {
// Common Node.js JSON shape for Buffers: { type: 'Buffer', data: [..bytes..] }
if (value && value.type === 'Buffer' && Array.isArray(value.data)) {
try {
const buf = Buffer.from(value.data);
const asBase64 = buf.toString('base64');
return asBase64 || null;
} catch (_) {
return undefined;
}
}
// Common frontend shapes:
// { kind: 'base64', base64: '...' }
// { kind: 'base64', data: '...' }
// { dataUrl: 'data:image/png;base64,...' }
// { value: '...' }
const candidates = [
value.base64,
value.data,
value.value,
value.content,
value.full,
value.raw,
value.payload,
value.src,
value.b64,
value.base64String,
value.dataUrl,
value.dataURL,
value.data_uri,
value.dataURI,
value.uri,
];
for (const c of candidates) {
if (typeof c === 'string' && c.trim()) {
let normalized = stripDataUri(c);
if (normalized.includes(' ') && !normalized.includes('+')) {
normalized = normalized.replace(/ /g, '+');
}
normalized = normalized.replace(/\s+/g, '');
return normalized || null;
}
}
// Fallback: scan shallow object for a base64-ish string
const looksLikeImageBase64 = (str) => {
const s = String(str || '').trim();
return s.startsWith('data:image/') || s.startsWith('iVBORw0KGgo') || s.startsWith('/9j/') || s.startsWith('R0lGOD');
};
for (const v of Object.values(value)) {
if (typeof v === 'string' && looksLikeImageBase64(v)) {
let normalized = stripDataUri(v);
if (normalized.includes(' ') && !normalized.includes('+')) {
normalized = normalized.replace(/ /g, '+');
}
normalized = normalized.replace(/\s+/g, '');
return normalized || null;
}
if (v && typeof v === 'object') {
for (const vv of Object.values(v)) {
if (typeof vv === 'string' && looksLikeImageBase64(vv)) {
let normalized = stripDataUri(vv);
if (normalized.includes(' ') && !normalized.includes('+')) {
normalized = normalized.replace(/ /g, '+');
}
normalized = normalized.replace(/\s+/g, '');
return normalized || null;
}
}
}
}
}
// Unknown shape; ignore instead of persisting garbage
return undefined;
function normalizeBase64ImageInput(rawValue, rawMimeType) {
if (rawValue === undefined && rawMimeType === undefined) {
return { provided: false };
}
if (rawValue === null || rawValue === '') {
return { provided: true, base64: null, mimeType: null };
}
if (typeof rawValue !== 'string') {
throw new Error('company_logo_base64 must be a string or null');
}
let value = rawValue.trim();
let mimeType = typeof rawMimeType === 'string' ? rawMimeType.trim() : '';
if (!value) {
return { provided: true, base64: null, mimeType: null };
}
const dataUriMatch = value.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=\r\n]+)$/);
if (dataUriMatch) {
mimeType = dataUriMatch[1];
value = dataUriMatch[2];
}
const compactBase64 = value.replace(/\s+/g, '');
if (!mimeType) {
throw new Error('company_logo_mime_type is required when company_logo_base64 is provided');
}
if (!ALLOWED_LOGO_MIME_TYPES.has(mimeType)) {
throw new Error(`Unsupported company logo type '${mimeType}'`);
}
if (!/^[A-Za-z0-9+/=]+$/.test(compactBase64)) {
throw new Error('company_logo_base64 is not valid base64');
}
const bytes = Buffer.from(compactBase64, 'base64').length;
if (bytes > MAX_LOGO_BYTES) {
throw new Error('Company logo exceeds 1 MB');
}
return {
provided: true,
base64: compactBase64,
mimeType,
};
}
class CompanySettingsController {
static async get(req, res) {
try {
const settings = await repo.get();
@ -114,8 +65,8 @@ class CompanySettingsController {
company_street: '',
company_postal_city: '',
company_country: '',
qr_code_60_base64: null,
qr_code_120_base64: null,
company_logo_base64: null,
company_logo_mime_type: null,
});
} catch (err) {
return res.status(500).json({ message: 'Failed to load company settings' });
@ -146,59 +97,21 @@ class CompanySettingsController {
});
}
const summarizeValue = (val) => {
const t = val === null ? 'null' : Array.isArray(val) ? 'array' : typeof val;
const summary = { type: t };
if (t === 'string') {
const s = String(val);
summary.length = s.length;
summary.hasDataUriPrefix = s.trim().toLowerCase().startsWith('data:image/');
summary.hasWhitespace = /\s/.test(s);
summary.startsWithPngSig = s.trim().startsWith('iVBORw0KGgo');
} else if (t === 'array') {
summary.length = val.length;
} else if (t === 'object' && val) {
summary.keys = Object.keys(val).slice(0, 30);
if (val.type === 'Buffer' && Array.isArray(val.data)) {
summary.bufferLike = true;
summary.dataLength = val.data.length;
}
}
return summary;
};
const hash12 = (s) => {
try {
if (typeof s !== 'string' || !s) return null;
return crypto.createHash('sha256').update(s).digest('hex').slice(0, 12);
} catch {
return null;
}
};
// Log request shape (not the base64 itself)
const incoming60Raw = body.qr_code_60_base64 ?? body.qrCode60Base64;
const incoming120Raw = body.qr_code_120_base64 ?? body.qrCode120Base64;
if (incoming60Raw !== undefined || incoming120Raw !== undefined) {
logger.info('companySettings:update:incoming_qr', {
requestId,
contentType: req.get('content-type') || null,
contentLength,
bodyKeys: Object.keys(body).slice(0, 50),
qr60: summarizeValue(incoming60Raw),
qr120: summarizeValue(incoming120Raw),
});
}
// Accept both snake_case and camelCase
const payload = {
company_name: body.company_name ?? body.companyName,
company_street: body.company_street ?? body.companyStreet,
company_postal_city: body.company_postal_city ?? body.companyPostalCity,
company_country: body.company_country ?? body.companyCountry,
qr_code_60_base64: CompanySettingsController._normalizeBase64ImageInput(body.qr_code_60_base64 ?? body.qrCode60Base64),
qr_code_120_base64: CompanySettingsController._normalizeBase64ImageInput(body.qr_code_120_base64 ?? body.qrCode120Base64),
};
const normalizedLogo = normalizeBase64ImageInput(
body.company_logo_base64 ?? body.companyLogoBase64,
body.company_logo_mime_type ?? body.companyLogoMimeType,
);
if (normalizedLogo.provided) {
payload.company_logo_base64 = normalizedLogo.base64;
payload.company_logo_mime_type = normalizedLogo.mimeType;
}
// Only forward keys that were actually provided (so we don't wipe values on partial updates)
const provided = {};
@ -206,52 +119,8 @@ class CompanySettingsController {
if (value !== undefined) provided[key] = value;
}
// Debug without leaking base64
if (incoming60Raw !== undefined && provided.qr_code_60_base64 === undefined) {
logger.warn('companySettings:update:qr60_ignored', {
requestId,
incoming: summarizeValue(incoming60Raw),
reason: 'normalize_returned_undefined_or_unrecognized_shape'
});
}
if (incoming120Raw !== undefined && provided.qr_code_120_base64 === undefined) {
logger.warn('companySettings:update:qr120_ignored', {
requestId,
incoming: summarizeValue(incoming120Raw),
reason: 'normalize_returned_undefined_or_unrecognized_shape'
});
}
if (provided.qr_code_60_base64 !== undefined || provided.qr_code_120_base64 !== undefined) {
const len60 = typeof provided.qr_code_60_base64 === 'string' ? provided.qr_code_60_base64.length : null;
const len120 = typeof provided.qr_code_120_base64 === 'string' ? provided.qr_code_120_base64.length : null;
logger.info('companySettings:update:qr_normalized', {
requestId,
has60: provided.qr_code_60_base64 !== undefined,
type60: provided.qr_code_60_base64 === null ? 'null' : typeof provided.qr_code_60_base64,
len60,
sha60: typeof provided.qr_code_60_base64 === 'string' ? hash12(provided.qr_code_60_base64) : null,
has120: provided.qr_code_120_base64 !== undefined,
type120: provided.qr_code_120_base64 === null ? 'null' : typeof provided.qr_code_120_base64,
len120,
sha120: typeof provided.qr_code_120_base64 === 'string' ? hash12(provided.qr_code_120_base64) : null,
});
}
const updated = await repo.update(provided);
if (updated && (provided.qr_code_60_base64 !== undefined || provided.qr_code_120_base64 !== undefined)) {
const storedLen60 = typeof updated.qr_code_60_base64 === 'string' ? updated.qr_code_60_base64.length : null;
const storedLen120 = typeof updated.qr_code_120_base64 === 'string' ? updated.qr_code_120_base64.length : null;
logger.info('companySettings:update:qr_stored', {
requestId,
storedLen60,
storedSha60: typeof updated.qr_code_60_base64 === 'string' ? hash12(updated.qr_code_60_base64) : null,
storedLen120,
storedSha120: typeof updated.qr_code_120_base64 === 'string' ? hash12(updated.qr_code_120_base64) : null,
});
}
return res.json(updated);
} catch (err) {
logger.error('companySettings:update:failed', {

View File

@ -0,0 +1,308 @@
const I18nPreferencesRepository = require('../../repositories/settings/I18nPreferencesRepository');
const { logger } = require('../../middleware/logger');
const repo = new I18nPreferencesRepository();
class I18nPreferencesController {
static _normalizeLanguageCode(value) {
const raw = String(value == null ? '' : value).trim();
if (!raw) return '';
const normalized = raw.replace('_', '-');
const pattern = /^[a-z]{2,3}(-[a-z0-9]{2,8})?$/i;
if (!pattern.test(normalized)) return null;
return normalized.toLowerCase();
}
static _normalizeStringArray(values) {
if (!Array.isArray(values)) return [];
const normalized = values
.map((v) => (v == null ? '' : String(v).trim()))
.filter(Boolean);
return [...new Set(normalized)];
}
static _normalizeCategories(categories) {
if (!Array.isArray(categories)) return [];
return categories
.map((item, idx) => {
const id = String(item?.id ?? '').trim() || `category_${idx + 1}`;
const label = String(item?.label ?? id).trim() || id;
const namespaces = I18nPreferencesController._normalizeStringArray(item?.namespaces);
const isCustom = Boolean(item?.isCustom);
return { id, label, namespaces, isCustom };
});
}
static _buildResponse(preferences) {
return {
ok: true,
preferences,
categories: preferences.categories,
globalKeys: preferences.globalKeys,
};
}
static _normalizeLanguagePayload(body) {
const source = (body?.language && typeof body.language === 'object') ? body.language : body;
const rawLanguageCode = source?.languageCode ?? source?.code ?? source?.lang;
const hasAnyLanguageField = rawLanguageCode !== undefined
|| source?.label !== undefined
|| source?.name !== undefined
|| source?.isEnabled !== undefined
|| source?.isCustom !== undefined;
if (!hasAnyLanguageField) return null;
const languageCode = I18nPreferencesController._normalizeLanguageCode(rawLanguageCode);
if (languageCode === null || !languageCode) {
throw new Error('Invalid language payload: languageCode is required');
}
const label = String(source?.label ?? source?.name ?? languageCode.toUpperCase()).trim();
const toBool = (v, fallback) => {
if (v === undefined || v === null) return fallback;
if (typeof v === 'boolean') return v;
const n = String(v).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(n)) return true;
if (['0', 'false', 'no', 'off'].includes(n)) return false;
return fallback;
};
return {
languageCode,
label,
isEnabled: toBool(source?.isEnabled, true),
isCustom: toBool(source?.isCustom, true),
};
}
static _normalizeTranslationsPayload(body, fallbackLanguageCode) {
const raw = body?.translations ?? body?.translationOverrides ?? body?.entries;
if (raw == null) return [];
const normalized = [];
if (Array.isArray(raw)) {
for (const item of raw) {
const languageCode = I18nPreferencesController._normalizeLanguageCode(
item?.languageCode ?? item?.lang ?? fallbackLanguageCode
);
const namespace = String(item?.namespace ?? '').trim();
const key = String(item?.key ?? item?.t_key ?? '').trim();
const value = item?.value ?? item?.t_value;
if (!languageCode || !namespace || !key || value === undefined) continue;
normalized.push({
languageCode,
namespace,
key,
value: String(value),
isCustom: item?.isCustom,
});
}
return normalized;
}
if (typeof raw === 'object') {
for (const [namespace, kv] of Object.entries(raw)) {
if (!kv || typeof kv !== 'object') continue;
for (const [key, value] of Object.entries(kv)) {
const languageCode = I18nPreferencesController._normalizeLanguageCode(fallbackLanguageCode);
if (!languageCode || !namespace || !key || value === undefined) continue;
normalized.push({
languageCode,
namespace: String(namespace).trim(),
key: String(key).trim(),
value: String(value),
isCustom: true,
});
}
}
}
return normalized;
}
static _extractPreferencesPayload(body) {
const source = (body?.preferences && typeof body.preferences === 'object')
? body.preferences
: body;
const hasCategories = Object.prototype.hasOwnProperty.call(source || {}, 'categories');
const hasGlobalKeys = Object.prototype.hasOwnProperty.call(source || {}, 'globalKeys');
return {
categories: hasCategories ? I18nPreferencesController._normalizeCategories(source?.categories) : undefined,
globalKeys: hasGlobalKeys ? I18nPreferencesController._normalizeStringArray(source?.globalKeys) : undefined,
};
}
static async _upsertBundle(req, res) {
try {
const userId = req.user?.userId ?? req.user?.id ?? null;
const language = I18nPreferencesController._normalizeLanguagePayload(req.body || {});
const fallbackLanguageCode = language?.languageCode
|| I18nPreferencesController._normalizeLanguageCode(req.body?.languageCode ?? req.body?.lang);
const translations = I18nPreferencesController._normalizeTranslationsPayload(req.body || {}, fallbackLanguageCode);
const extracted = I18nPreferencesController._extractPreferencesPayload(req.body || {});
const result = await repo.upsertBundle({
categories: extracted.categories,
globalKeys: extracted.globalKeys,
language,
translations,
updatedByUserId: userId,
});
return res.status(200).json({
...I18nPreferencesController._buildResponse(result.preferences),
language: result.language || null,
translationsUpserted: result.translationsUpserted || 0,
});
} catch (error) {
if (String(error?.message || '').includes('Invalid language payload')) {
return res.status(400).json({ ok: false, message: error.message });
}
logger.error('i18nPreferences:upsertBundle:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to upsert i18n language data' });
}
}
static async get(req, res) {
try {
const preferences = await repo.get();
return res.status(200).json(I18nPreferencesController._buildResponse(preferences));
} catch (error) {
logger.error('i18nPreferences:get:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to load i18n preferences' });
}
}
static async post(req, res) {
return I18nPreferencesController._upsertBundle(req, res);
}
static async put(req, res) {
return I18nPreferencesController._upsertBundle(req, res);
}
static async delete(req, res) {
try {
const requestedLanguageCode = I18nPreferencesController._normalizeLanguageCode(req.query?.languageCode);
if (requestedLanguageCode === null) {
return res.status(400).json({ ok: false, message: 'Invalid languageCode query parameter' });
}
if (requestedLanguageCode) {
const result = await repo.deleteLanguageEntries(
requestedLanguageCode,
req.user?.userId ?? req.user?.id ?? null
);
return res.status(200).json({
ok: true,
languageCode: requestedLanguageCode,
deletedRows: result.deletedRows,
touchedTables: result.touchedTables,
});
}
const hasBodyReplacement = req.body && (
Object.prototype.hasOwnProperty.call(req.body, 'categories') ||
Object.prototype.hasOwnProperty.call(req.body, 'globalKeys')
);
const preferences = hasBodyReplacement
? await repo.upsert({
categories: I18nPreferencesController._normalizeCategories(req.body?.categories),
globalKeys: I18nPreferencesController._normalizeStringArray(req.body?.globalKeys),
updatedByUserId: req.user?.userId ?? req.user?.id ?? null,
})
: await repo.clear(req.user?.userId ?? req.user?.id ?? null);
return res.status(200).json(I18nPreferencesController._buildResponse(preferences));
} catch (error) {
logger.error('i18nPreferences:delete:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to delete i18n preferences' });
}
}
static async getTranslations(req, res) {
try {
const languageCodeRaw = req.query?.languageCode ?? req.query?.lang;
const normalizedLanguageCode = languageCodeRaw
? I18nPreferencesController._normalizeLanguageCode(languageCodeRaw)
: '';
if (normalizedLanguageCode === null) {
return res.status(400).json({ ok: false, message: 'Invalid languageCode query parameter' });
}
const namespace = String(req.query?.namespace ?? '').trim() || undefined;
const translations = await repo.listTranslations({
languageCode: normalizedLanguageCode || undefined,
namespace,
});
return res.status(200).json({ ok: true, translations });
} catch (error) {
logger.error('i18nTranslations:get:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to load translations' });
}
}
static async upsertTranslations(req, res) {
try {
const language = I18nPreferencesController._normalizeLanguagePayload(req.body || {});
const fallbackLanguageCode = language?.languageCode
|| I18nPreferencesController._normalizeLanguageCode(req.body?.languageCode ?? req.body?.lang);
const translations = I18nPreferencesController._normalizeTranslationsPayload(req.body || {}, fallbackLanguageCode);
if (!translations.length) {
return res.status(400).json({ ok: false, message: 'No valid translations provided' });
}
const userId = req.user?.userId ?? req.user?.id ?? null;
const result = await repo.upsertTranslations({ translations, updatedByUserId: userId });
if (language) {
await repo.upsertBundle({ language, updatedByUserId: userId });
}
return res.status(200).json({
ok: true,
translationsUpserted: result.upsertedCount || 0,
});
} catch (error) {
if (String(error?.message || '').includes('Invalid language payload')) {
return res.status(400).json({ ok: false, message: error.message });
}
logger.error('i18nTranslations:upsert:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to upsert translations' });
}
}
static async scan(req, res) {
try {
const languageCodeRaw = req.query?.languageCode ?? req.query?.lang ?? req.body?.languageCode ?? req.body?.lang;
const normalizedLanguageCode = languageCodeRaw
? I18nPreferencesController._normalizeLanguageCode(languageCodeRaw)
: '';
if (normalizedLanguageCode === null) {
return res.status(400).json({ ok: false, message: 'Invalid languageCode parameter' });
}
const summary = await repo.getScanSummary({ languageCode: normalizedLanguageCode || undefined });
return res.status(200).json({
ok: true,
...summary,
});
} catch (error) {
logger.error('i18nScan:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to scan i18n data' });
}
}
}
module.exports = I18nPreferencesController;

View File

@ -0,0 +1,99 @@
const MailTemplateService = require('../../services/template/MailTemplateService');
class MailTemplatesController {
static _currentUserId(req) {
return req.user?.userId ?? req.user?.id ?? null;
}
static async list(req, res) {
try {
const data = await MailTemplateService.list(req.query || {});
return res.json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to list mail templates' });
}
}
static async getById(req, res) {
try {
const data = await MailTemplateService.getById(req.params.id);
if (!data) {
return res.status(404).json({ success: false, message: 'Mail template not found' });
}
return res.json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to load mail template' });
}
}
static async create(req, res) {
try {
const data = await MailTemplateService.create(req.body || {}, MailTemplatesController._currentUserId(req));
return res.status(201).json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to create mail template' });
}
}
static async update(req, res) {
try {
const data = await MailTemplateService.update(req.params.id, req.body || {}, MailTemplatesController._currentUserId(req));
if (!data) {
return res.status(404).json({ success: false, message: 'Mail template not found' });
}
return res.json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to update mail template' });
}
}
static async activate(req, res) {
try {
const data = await MailTemplateService.activate(req.params.id, MailTemplatesController._currentUserId(req));
if (!data) {
return res.status(404).json({ success: false, message: 'Mail template not found' });
}
return res.json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to activate mail template' });
}
}
static async archive(req, res) {
try {
const data = await MailTemplateService.archive(req.params.id, MailTemplatesController._currentUserId(req));
if (!data) {
return res.status(404).json({ success: false, message: 'Mail template not found' });
}
return res.json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to archive mail template' });
}
}
static async unarchive(req, res) {
try {
const data = await MailTemplateService.unarchive(req.params.id, MailTemplatesController._currentUserId(req));
if (!data) {
return res.status(404).json({ success: false, message: 'Mail template not found' });
}
return res.json({ success: true, data });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to unarchive mail template' });
}
}
static async remove(req, res) {
try {
const deleted = await MailTemplateService.remove(req.params.id);
if (!deleted) {
return res.status(404).json({ success: false, message: 'Mail template not found' });
}
return res.json({ success: true });
} catch (error) {
return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to delete mail template' });
}
}
}
module.exports = MailTemplatesController;

View File

@ -36,8 +36,8 @@ function saveDebugFile(filename, data, encoding = 'utf8') {
// Helper to remove/empty placeholders except allow-list
// Updated: match any content inside {{ ... }} (not only \w+) so placeholders like {{company.name}} are sanitized.
// allowList contains exact placeholder names to preserve (e.g. 'companyStamp', 'profitplanetSignature').
function sanitizePlaceholders(html, allowList = ['currentDate','companyStamp','companyStampInline','companyStampSmall','profitplanetSignature']) {
// allowList contains exact placeholder names to preserve (e.g. 'companyStamp', 'companyLogo', 'profitplanetSignature').
function sanitizePlaceholders(html, allowList = ['currentDate','companyStamp','companyStampInline','companyStampSmall','companyLogo','profitplanetSignature']) {
if (!html || typeof html !== 'string') return html;
const allowSet = new Set((allowList || []).map(s => String(s).trim()).filter(Boolean));
@ -412,7 +412,7 @@ async function renderLatestActiveContractHtmlForUser({ targetUserId, userType, c
});
html = html.replace(/{{\s*signatureImage\s*}}/g, 'Your signature will appear here');
html = sanitizePlaceholders(html, ['companyStamp','companyStampInline','companyStampSmall','profitplanetSignature']);
html = sanitizePlaceholders(html, ['companyStamp','companyStampInline','companyStampSmall','companyLogo','profitplanetSignature']);
const reqForStamp = (req && req.user) ? req : { user: { id: targetUserId, user_type: userType } };
try { html = await applyCompanyStampPlaceholders(html, reqForStamp); } catch (e) {}
@ -2101,7 +2101,7 @@ exports.downloadPdf = async (req, res) => {
// SANITIZE: remove variables for downloaded PDF.
// Allow only stamp/signature placeholders to remain so images can still be injected.
html = sanitizePlaceholders(html, ['companyStamp','companyStampInline','companyStampSmall','profitplanetSignature']);
html = sanitizePlaceholders(html, ['companyStamp','companyStampInline','companyStampSmall','companyLogo','profitplanetSignature']);
// Do NOT keep currentDate for download (user requested variables emptied)
// Apply company stamp & profitplanet signature (these placeholders were preserved above)

View File

@ -30,6 +30,16 @@ module.exports = {
}
},
async revenueSummary(req, res) {
try {
const data = await service.getRevenueSummary();
return res.json({ success: true, data });
} catch (e) {
console.error('[INVOICE REVENUE SUMMARY]', e);
return res.status(403).json({ success: false, message: e.message });
}
},
async pay(req, res) {
try {
const data = await service.markPaid(req.params.id, {
@ -96,4 +106,34 @@ module.exports = {
return res.status(400).json({ success: false, message: e.message });
}
},
async adminCreate(req, res) {
try {
const fields = {
buyer_name: req.body.buyer_name,
buyer_email: req.body.buyer_email,
buyer_street: req.body.buyer_street,
buyer_postal_code: req.body.buyer_postal_code,
buyer_city: req.body.buyer_city,
buyer_country: req.body.buyer_country,
currency: req.body.currency || 'EUR',
total_net: req.body.total_net,
total_tax: req.body.total_tax,
total_gross: req.body.total_gross,
vat_rate: req.body.vat_rate,
status: req.body.status || 'issued',
issued_at: req.body.issued_at,
due_at: req.body.due_at,
};
if (!fields.total_gross || isNaN(Number(fields.total_gross))) {
return res.status(400).json({ success: false, message: 'total_gross is required and must be a number' });
}
const pdfBuffer = req.file ? req.file.buffer : null;
const data = await service.adminCreateManual(fields, pdfBuffer);
return res.status(201).json({ success: true, data });
} catch (e) {
console.error('[INVOICE ADMIN CREATE]', e);
return res.status(400).json({ success: false, message: e.message });
}
},
};

View File

@ -261,6 +261,56 @@ class LoginController {
return res.status(500).json({ success: false, message: 'Internal server error' });
}
}
static async validate(req, res) {
try {
const refreshToken = req.cookies?.refreshToken;
if (!refreshToken) {
return res.status(401).json({ ok: false, message: 'No refresh token provided' });
}
const result = await LoginService.validateByRefreshToken(refreshToken);
const user = result?.user || {};
const role = String(user.role || '').toLowerCase();
const userRoles = Array.isArray(user.roles)
? user.roles.map((r) => String(r).toLowerCase())
: [];
const mergedRoles = [...new Set([role, ...userRoles].filter(Boolean))];
const isAdmin = Boolean(
user.isAdmin ||
role === 'admin' ||
role === 'super_admin' ||
role === 'superadmin' ||
mergedRoles.includes('admin') ||
mergedRoles.includes('super_admin') ||
mergedRoles.includes('superadmin')
);
return res.status(200).json({
ok: true,
user: {
id: String(user.id ?? ''),
role: user.role || null,
isAdmin,
roles: mergedRoles,
},
isAdmin,
role: user.role || null,
roles: mergedRoles,
});
} catch (error) {
if (error.status) {
return res.status(error.status).json({ ok: false, message: error.message });
}
logger.error('authValidate:error', {
message: error?.message,
stack: error?.stack,
});
return res.status(500).json({ ok: false, message: 'Internal server error' });
}
}
}
module.exports = LoginController;

View File

@ -94,6 +94,13 @@ async function addColumnIfMissing(conn, table, column, ddlFragment /* includes t
return true;
}
async function dropColumnIfExists(conn, table, column) {
if (!(await columnExists(conn, table, column))) return false;
await conn.query(`ALTER TABLE \`${table}\` DROP COLUMN \`${column}\``);
console.log(`🗑️ Dropped column ${table}.${column}`);
return true;
}
async function constraintExists(conn, table, constraintName) {
const [rows] = await conn.query(
`SELECT 1
@ -451,6 +458,30 @@ const createDatabase = async () => {
`);
console.log('✅ Document templates table created/verified');
await connection.query(`
CREATE TABLE IF NOT EXISTS mail_templates (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
template_type VARCHAR(100) NOT NULL,
name VARCHAR(255) NOT NULL,
subject VARCHAR(255) NULL,
html_content LONGTEXT NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 0,
is_archived TINYINT(1) NOT NULL DEFAULT 0,
archived_at TIMESTAMP NULL,
created_by INT NULL,
updated_by INT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_mail_templates_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT fk_mail_templates_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
INDEX idx_mail_templates_type (template_type),
INDEX idx_mail_templates_active (is_active),
INDEX idx_mail_templates_archived (is_archived),
INDEX idx_mail_templates_type_active (template_type, is_active)
);
`);
console.log('✅ Mail templates table created/verified');
// Ensure enum includes 'abo' on existing schemas
try {
await connection.query(`
@ -812,9 +843,9 @@ const createDatabase = async () => {
company_name VARCHAR(200) NOT NULL DEFAULT 'ProfitPlanet GmbH',
company_street VARCHAR(255) NOT NULL DEFAULT '',
company_postal_city VARCHAR(255) NOT NULL DEFAULT '',
company_country VARCHAR(100) NOT NULL DEFAULT 'Germany',
qr_code_60_base64 LONGTEXT NULL,
qr_code_120_base64 LONGTEXT NULL,
company_country VARCHAR(100) NOT NULL DEFAULT 'Austria',
company_logo_base64 MEDIUMTEXT NULL,
company_logo_mime_type VARCHAR(100) NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CHECK (id = 1)
);
@ -823,11 +854,89 @@ const createDatabase = async () => {
await connection.query(`
INSERT IGNORE INTO company_settings (id) VALUES (1);
`);
await addColumnIfMissing(connection, 'company_settings', 'company_logo_base64', `MEDIUMTEXT NULL AFTER company_country`);
await addColumnIfMissing(connection, 'company_settings', 'company_logo_mime_type', `VARCHAR(100) NULL AFTER company_logo_base64`);
console.log('✅ Company settings table created/verified');
// Backward-compatible: add QR code columns if missing
await addColumnIfMissing(connection, 'company_settings', 'qr_code_60_base64', 'LONGTEXT NULL');
await addColumnIfMissing(connection, 'company_settings', 'qr_code_120_base64', 'LONGTEXT NULL');
// Cleanup legacy invoice QR columns, which are no longer used.
await dropColumnIfExists(connection, 'company_settings', 'qr_code_60_base64');
await dropColumnIfExists(connection, 'company_settings', 'qr_code_120_base64');
// --- I18n Preferences (single-row, admin language-management settings) ---
await connection.query(`
CREATE TABLE IF NOT EXISTS i18n_preferences (
id TINYINT PRIMARY KEY DEFAULT 1,
categories_json JSON NULL,
global_keys_json JSON NULL,
updated_by_user_id INT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT chk_i18n_preferences_singleton CHECK (id = 1),
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
);
`);
await connection.query(`
INSERT IGNORE INTO i18n_preferences (id, categories_json, global_keys_json, updated_by_user_id)
VALUES (1, JSON_ARRAY(), JSON_ARRAY(), NULL);
`);
console.log('✅ i18n preferences table created/verified');
// Backward-compatible for older schemas
await addColumnIfMissing(connection, 'i18n_preferences', 'categories_json', 'JSON NULL');
await addColumnIfMissing(connection, 'i18n_preferences', 'global_keys_json', 'JSON NULL');
await addColumnIfMissing(connection, 'i18n_preferences', 'updated_by_user_id', 'INT NULL');
await addForeignKeyIfMissing(
connection,
'i18n_preferences',
'i18n_preferences_ibfk_1',
`ALTER TABLE \`i18n_preferences\`
ADD CONSTRAINT \`i18n_preferences_ibfk_1\` FOREIGN KEY (\`updated_by_user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE SET NULL ON UPDATE CASCADE`
);
// Language metadata used by language-management UI
await connection.query(`
CREATE TABLE IF NOT EXISTS i18n_languages (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
language_code VARCHAR(16) NOT NULL,
label VARCHAR(100) NULL,
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
is_custom BOOLEAN NOT NULL DEFAULT FALSE,
created_by_user_id INT NULL,
updated_by_user_id INT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uq_i18n_languages_code UNIQUE (language_code),
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
);
`);
console.log('✅ i18n languages table created/verified');
// Translation overrides/custom values per language
await connection.query(`
CREATE TABLE IF NOT EXISTS i18n_translation_overrides (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
language_code VARCHAR(16) NOT NULL,
namespace VARCHAR(128) NOT NULL,
t_key VARCHAR(255) NOT NULL,
t_value LONGTEXT NOT NULL,
is_custom BOOLEAN NOT NULL DEFAULT TRUE,
created_by_user_id INT NULL,
updated_by_user_id INT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uq_i18n_translation_overrides UNIQUE (language_code, namespace, t_key),
FOREIGN KEY (language_code) REFERENCES i18n_languages(language_code) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
);
`);
console.log('✅ i18n translation overrides table created/verified');
await ensureIndex(connection, 'i18n_translation_overrides', 'idx_i18n_overrides_lang', '`language_code`');
await ensureIndex(connection, 'i18n_translation_overrides', 'idx_i18n_overrides_namespace', '`namespace`');
await ensureIndex(connection, 'i18n_translation_overrides', 'idx_i18n_overrides_lang_namespace', '`language_code`, `namespace`');
// --- Dashboard Platforms (admin managed dashboard cards) ---
await connection.query(`
@ -1035,6 +1144,21 @@ const createDatabase = async () => {
`);
console.log('✅ Coffee table (simplified) created/verified');
await connection.query(`
CREATE TABLE IF NOT EXISTS coffee_table_images (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
coffee_id BIGINT NOT NULL,
object_storage_id VARCHAR(255) NOT NULL,
original_filename VARCHAR(255) NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_coffee_table_images_coffee FOREIGN KEY (coffee_id) REFERENCES coffee_table(id) ON DELETE CASCADE ON UPDATE CASCADE,
INDEX idx_coffee_table_images_coffee_sort (coffee_id, sort_order),
INDEX idx_coffee_table_images_key (object_storage_id)
);
`);
console.log('✅ Coffee table images created/verified');
// --- Coffee shipping fees (fixed package sizes) ---
await connection.query(`
CREATE TABLE IF NOT EXISTS coffee_shipping_fees (
@ -1187,7 +1311,7 @@ const createDatabase = async () => {
total_tax DECIMAL(12,2) NOT NULL DEFAULT 0.00,
total_gross DECIMAL(12,2) NOT NULL DEFAULT 0.00,
vat_rate DECIMAL(6,3) NULL,
status ENUM('draft','issued','paid','canceled') NOT NULL DEFAULT 'draft',
status ENUM('draft','issued','paid','overdue','canceled') NOT NULL DEFAULT 'draft',
issued_at DATETIME NULL,
due_at DATETIME NULL,
pdf_storage_key VARCHAR(255) NULL,
@ -1202,6 +1326,16 @@ const createDatabase = async () => {
`);
console.log('✅ Invoices table created/verified');
try {
await connection.query(`
ALTER TABLE invoices
MODIFY COLUMN status ENUM('draft','issued','paid','overdue','canceled') NOT NULL DEFAULT 'draft'
`);
console.log('✅ Updated invoices.status column to include overdue');
} catch (err) {
console.warn('⚠️ Could not modify invoices.status column:', err.message);
}
await connection.query(`
CREATE TABLE IF NOT EXISTS invoice_items (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
@ -1730,6 +1864,12 @@ const createDatabase = async () => {
await ensureIndex(connection, 'rate_limit', 'idx_rate_limit_rate_key', 'rate_key');
await ensureIndex(connection, 'document_templates', 'idx_document_templates_user_type', 'user_type');
await ensureIndex(connection, 'document_templates', 'idx_document_templates_state_user_type', 'state, user_type');
await ensureIndex(connection, 'mail_templates', 'idx_mail_templates_type', 'template_type');
await ensureIndex(connection, 'mail_templates', 'idx_mail_templates_active', 'is_active');
await ensureIndex(connection, 'mail_templates', 'idx_mail_templates_archived', 'is_archived');
await ensureIndex(connection, 'mail_templates', 'idx_mail_templates_type_active', 'template_type, is_active');
await ensureIndex(connection, 'coffee_table_images', 'idx_coffee_table_images_coffee_sort', 'coffee_id, sort_order');
await ensureIndex(connection, 'coffee_table_images', 'idx_coffee_table_images_key', 'object_storage_id');
await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_company', 'company_id');
await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_active', 'is_active');
console.log('🚀 Performance indexes created/verified');

3
package-lock.json generated
View File

@ -2644,7 +2644,8 @@
"version": "0.0.1581282",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/dfa": {
"version": "1.2.0",

View File

@ -206,8 +206,35 @@ class InvoiceRepository {
return rows.map((r) => new Invoice(r));
}
async getPaidRevenueSummary() {
const [rows] = await pool.query(
`SELECT COALESCE(SUM(total_gross), 0) AS total_paid_all_time,
COALESCE(MAX(currency), 'EUR') AS currency,
COUNT(*) AS paid_invoice_count
FROM invoices
WHERE status = 'paid'`
);
return {
total_paid_all_time: Number(rows?.[0]?.total_paid_all_time || 0),
currency: rows?.[0]?.currency || 'EUR',
paid_invoice_count: Number(rows?.[0]?.paid_invoice_count || 0),
};
}
async markIssuedPastDueAsOverdue() {
const [result] = await pool.query(
`UPDATE invoices
SET status = 'overdue', updated_at = NOW()
WHERE status = 'issued'
AND due_at IS NOT NULL
AND due_at < CURDATE()`
);
return result?.affectedRows || 0;
}
async updateStatus(invoiceId, newStatus) {
const allowed = ['draft', 'issued', 'paid', 'canceled'];
const allowed = ['draft', 'issued', 'paid', 'overdue', 'canceled'];
if (!allowed.includes(newStatus)) {
throw new Error(`Invalid status '${newStatus}'. Allowed: ${allowed.join(', ')}`);
}
@ -225,6 +252,57 @@ class InvoiceRepository {
);
return rows || [];
}
async createManualInvoice({
source_type = 'manual',
source_id = null,
user_id = null,
buyer_name = null,
buyer_email = null,
buyer_street = null,
buyer_postal_code = null,
buyer_city = null,
buyer_country = null,
currency = 'EUR',
total_net = 0,
total_tax = 0,
total_gross = 0,
vat_rate = null,
status = 'issued',
issued_at = null,
due_at = null,
context = null,
}) {
const invoice_number = await genInvoiceNumber();
const [res] = await pool.query(
`INSERT INTO invoices
(invoice_number, user_id, source_type, source_id, buyer_name, buyer_email, buyer_street, buyer_postal_code, buyer_city, buyer_country,
currency, total_net, total_tax, total_gross, vat_rate, status, issued_at, due_at, pdf_storage_key, context, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, NOW(), NOW())`,
[
invoice_number,
user_id || null,
source_type,
source_id || 0,
buyer_name || null,
buyer_email || null,
buyer_street || null,
buyer_postal_code || null,
buyer_city || null,
buyer_country || null,
currency,
+Number(total_net).toFixed(2),
+Number(total_tax).toFixed(2),
+Number(total_gross).toFixed(2),
vat_rate != null ? Number(vat_rate) : null,
status,
issued_at || null,
due_at || null,
context ? JSON.stringify(context) : null,
],
);
return this.getById(res.insertId);
}
}
module.exports = InvoiceRepository;

View File

@ -2,7 +2,12 @@ const pool = require('../../database/database');
class CompanySettingsRepository {
async get() {
const [rows] = await pool.query('SELECT * FROM company_settings WHERE id = 1');
const [rows] = await pool.query(
`SELECT id, company_name, company_street, company_postal_city, company_country,
company_logo_base64, company_logo_mime_type, updated_at
FROM company_settings
WHERE id = 1`
);
return rows[0] || null;
}
@ -11,8 +16,8 @@ class CompanySettingsRepository {
company_street,
company_postal_city,
company_country,
qr_code_60_base64,
qr_code_120_base64,
company_logo_base64,
company_logo_mime_type,
} = {}) {
const current = await this.get();
const next = {
@ -20,27 +25,30 @@ class CompanySettingsRepository {
company_street: company_street !== undefined ? company_street : (current?.company_street ?? ''),
company_postal_city: company_postal_city !== undefined ? company_postal_city : (current?.company_postal_city ?? ''),
company_country: company_country !== undefined ? company_country : (current?.company_country ?? ''),
qr_code_60_base64: qr_code_60_base64 !== undefined ? qr_code_60_base64 : (current?.qr_code_60_base64 ?? null),
qr_code_120_base64: qr_code_120_base64 !== undefined ? qr_code_120_base64 : (current?.qr_code_120_base64 ?? null),
company_logo_base64: company_logo_base64 !== undefined ? (company_logo_base64 || null) : (current?.company_logo_base64 ?? null),
company_logo_mime_type: company_logo_mime_type !== undefined ? (company_logo_mime_type || null) : (current?.company_logo_mime_type ?? null),
};
await pool.query(
`INSERT INTO company_settings (id, company_name, company_street, company_postal_city, company_country, qr_code_60_base64, qr_code_120_base64)
`INSERT INTO company_settings (
id, company_name, company_street, company_postal_city, company_country,
company_logo_base64, company_logo_mime_type
)
VALUES (1, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
company_name = VALUES(company_name),
company_street = VALUES(company_street),
company_postal_city = VALUES(company_postal_city),
company_country = VALUES(company_country),
qr_code_60_base64 = VALUES(qr_code_60_base64),
qr_code_120_base64 = VALUES(qr_code_120_base64)`,
company_logo_base64 = VALUES(company_logo_base64),
company_logo_mime_type = VALUES(company_logo_mime_type)`,
[
next.company_name || '',
next.company_street || '',
next.company_postal_city || '',
next.company_country || '',
next.qr_code_60_base64 ?? null,
next.qr_code_120_base64 ?? null,
next.company_logo_base64,
next.company_logo_mime_type,
]
);
return this.get();

View File

@ -0,0 +1,421 @@
const db = require('../../database/database');
class I18nPreferencesRepository {
_safeJsonArray(value) {
if (Array.isArray(value)) return value;
if (value == null) return [];
try {
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
return Array.isArray(parsed) ? parsed : [];
} catch (_) {
return [];
}
}
_normalizeRow(row) {
const categories = this._safeJsonArray(row?.categories_json);
const globalKeys = this._safeJsonArray(row?.global_keys_json);
return {
categories,
globalKeys,
};
}
_safeBoolean(value, fallback) {
if (value === undefined || value === null) return fallback;
if (typeof value === 'boolean') return value;
const normalized = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return fallback;
}
async get() {
const [rows] = await db.query('SELECT * FROM i18n_preferences WHERE id = 1 LIMIT 1');
if (!rows.length) {
return { categories: [], globalKeys: [] };
}
return this._normalizeRow(rows[0]);
}
async upsert({ categories, globalKeys, updatedByUserId } = {}) {
const current = await this.get();
const nextCategories = categories !== undefined ? categories : current.categories;
const nextGlobalKeys = globalKeys !== undefined ? globalKeys : current.globalKeys;
await db.query(
`INSERT INTO i18n_preferences (id, categories_json, global_keys_json, updated_by_user_id)
VALUES (1, ?, ?, ?)
ON DUPLICATE KEY UPDATE
categories_json = VALUES(categories_json),
global_keys_json = VALUES(global_keys_json),
updated_by_user_id = VALUES(updated_by_user_id)`,
[JSON.stringify(nextCategories || []), JSON.stringify(nextGlobalKeys || []), updatedByUserId || null]
);
return this.get();
}
async clear(updatedByUserId) {
await db.query(
`INSERT INTO i18n_preferences (id, categories_json, global_keys_json, updated_by_user_id)
VALUES (1, ?, ?, ?)
ON DUPLICATE KEY UPDATE
categories_json = VALUES(categories_json),
global_keys_json = VALUES(global_keys_json),
updated_by_user_id = VALUES(updated_by_user_id)`,
[JSON.stringify([]), JSON.stringify([]), updatedByUserId || null]
);
return this.get();
}
async _tableExists(conn, tableName) {
const [rows] = await conn.query(
`SELECT 1
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
LIMIT 1`,
[tableName]
);
return rows.length > 0;
}
async _columnExists(conn, tableName, columnName) {
const [rows] = await conn.query(
`SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND COLUMN_NAME = ?
LIMIT 1`,
[tableName, columnName]
);
return rows.length > 0;
}
async deleteLanguageEntries(languageCode, updatedByUserId) {
const conn = await db.getConnection();
const safeLanguageCode = String(languageCode || '').trim();
const targets = [
// Language metadata
{ table: 'i18n_languages', possibleColumns: ['language_code', 'lang', 'code'] },
{ table: 'i18n_language_metadata', possibleColumns: ['language_code', 'lang', 'code'] },
// Translation/custom-value stores
{ table: 'i18n_translation_overrides', possibleColumns: ['language_code', 'lang'] },
{ table: 'i18n_translations', possibleColumns: ['language_code', 'lang'] },
// Potential language-scoped preference/link tables
{ table: 'i18n_preferences_languages', possibleColumns: ['language_code', 'lang'] },
{ table: 'i18n_preference_categories', possibleColumns: ['language_code', 'lang'] },
{ table: 'i18n_preference_global_keys', possibleColumns: ['language_code', 'lang'] },
];
let deletedRows = 0;
const touchedTables = [];
try {
await conn.beginTransaction();
for (const target of targets) {
const exists = await this._tableExists(conn, target.table);
if (!exists) continue;
let deleteColumn = null;
for (const col of target.possibleColumns) {
if (await this._columnExists(conn, target.table, col)) {
deleteColumn = col;
break;
}
}
if (!deleteColumn) continue;
const [result] = await conn.query(
`DELETE FROM \`${target.table}\` WHERE \`${deleteColumn}\` = ?`,
[safeLanguageCode]
);
const affected = Number(result?.affectedRows || 0);
if (affected > 0) {
deletedRows += affected;
touchedTables.push(target.table);
}
}
if (await this._tableExists(conn, 'i18n_preferences')) {
await conn.query(
`UPDATE i18n_preferences
SET updated_by_user_id = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = 1`,
[updatedByUserId || null]
);
}
await conn.commit();
return { deletedRows, touchedTables };
} catch (error) {
await conn.rollback();
throw error;
} finally {
conn.release();
}
}
async upsertLanguage(conn, language, updatedByUserId) {
if (!language || !language.languageCode) return null;
const languageCode = String(language.languageCode).trim().toLowerCase();
const label = String(language.label || languageCode).trim();
const isEnabled = this._safeBoolean(language.isEnabled, true);
const isCustom = this._safeBoolean(language.isCustom, true);
await conn.query(
`INSERT INTO i18n_languages (language_code, label, is_enabled, is_custom, created_by_user_id, updated_by_user_id)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
label = VALUES(label),
is_enabled = VALUES(is_enabled),
is_custom = VALUES(is_custom),
updated_by_user_id = VALUES(updated_by_user_id)`,
[languageCode, label, isEnabled ? 1 : 0, isCustom ? 1 : 0, updatedByUserId || null, updatedByUserId || null]
);
const [rows] = await conn.query(
`SELECT language_code AS languageCode,
label,
is_enabled AS isEnabled,
is_custom AS isCustom,
created_at AS createdAt,
updated_at AS updatedAt
FROM i18n_languages
WHERE language_code = ?
LIMIT 1`,
[languageCode]
);
return rows[0] || null;
}
async upsertTranslationOverrides(conn, translationEntries, updatedByUserId) {
if (!Array.isArray(translationEntries) || !translationEntries.length) {
return { upsertedCount: 0 };
}
let upsertedCount = 0;
for (const entry of translationEntries) {
const languageCode = String(entry.languageCode || '').trim().toLowerCase();
const namespace = String(entry.namespace || '').trim();
const key = String(entry.key || '').trim();
const value = entry.value == null ? '' : String(entry.value);
const isCustom = this._safeBoolean(entry.isCustom, true);
if (!languageCode || !namespace || !key) continue;
await conn.query(
`INSERT INTO i18n_translation_overrides
(language_code, namespace, t_key, t_value, is_custom, created_by_user_id, updated_by_user_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
t_value = VALUES(t_value),
is_custom = VALUES(is_custom),
updated_by_user_id = VALUES(updated_by_user_id)`,
[
languageCode,
namespace,
key,
value,
isCustom ? 1 : 0,
updatedByUserId || null,
updatedByUserId || null,
]
);
upsertedCount += 1;
}
return { upsertedCount };
}
async upsertBundle({ categories, globalKeys, language, translations, updatedByUserId } = {}) {
const conn = await db.getConnection();
try {
await conn.beginTransaction();
const [prefRows] = await conn.query('SELECT * FROM i18n_preferences WHERE id = 1 LIMIT 1');
const current = prefRows.length
? this._normalizeRow(prefRows[0])
: { categories: [], globalKeys: [] };
const nextCategories = categories !== undefined ? categories : current.categories;
const nextGlobalKeys = globalKeys !== undefined ? globalKeys : current.globalKeys;
await conn.query(
`INSERT INTO i18n_preferences (id, categories_json, global_keys_json, updated_by_user_id)
VALUES (1, ?, ?, ?)
ON DUPLICATE KEY UPDATE
categories_json = VALUES(categories_json),
global_keys_json = VALUES(global_keys_json),
updated_by_user_id = VALUES(updated_by_user_id)`,
[JSON.stringify(nextCategories || []), JSON.stringify(nextGlobalKeys || []), updatedByUserId || null]
);
const languagesToEnsure = new Map();
if (language && language.languageCode) {
languagesToEnsure.set(String(language.languageCode).toLowerCase(), language);
}
for (const t of (Array.isArray(translations) ? translations : [])) {
const code = String(t.languageCode || '').trim().toLowerCase();
if (!code) continue;
if (!languagesToEnsure.has(code)) {
languagesToEnsure.set(code, {
languageCode: code,
label: code.toUpperCase(),
isEnabled: true,
isCustom: true,
});
}
}
let upsertedLanguage = null;
for (const [, langPayload] of languagesToEnsure) {
const row = await this.upsertLanguage(conn, langPayload, updatedByUserId);
if (language && row && row.languageCode === String(language.languageCode).toLowerCase()) {
upsertedLanguage = row;
}
}
const translationResult = await this.upsertTranslationOverrides(conn, translations, updatedByUserId);
const [savedRows] = await conn.query('SELECT * FROM i18n_preferences WHERE id = 1 LIMIT 1');
const preferences = savedRows.length
? this._normalizeRow(savedRows[0])
: { categories: [], globalKeys: [] };
await conn.commit();
return {
preferences,
language: upsertedLanguage,
translationsUpserted: translationResult.upsertedCount,
};
} catch (error) {
await conn.rollback();
throw error;
} finally {
conn.release();
}
}
async listTranslations({ languageCode, namespace } = {}) {
const filters = [];
const params = [];
if (languageCode) {
filters.push('language_code = ?');
params.push(String(languageCode).trim().toLowerCase());
}
if (namespace) {
filters.push('namespace = ?');
params.push(String(namespace).trim());
}
const whereClause = filters.length ? `WHERE ${filters.join(' AND ')}` : '';
const [rows] = await db.query(
`SELECT language_code AS languageCode,
namespace,
t_key AS \`key\`,
t_value AS value,
is_custom AS isCustom,
updated_at AS updatedAt
FROM i18n_translation_overrides
${whereClause}
ORDER BY language_code, namespace, t_key`,
params
);
return rows || [];
}
async upsertTranslations({ translations, updatedByUserId } = {}) {
const conn = await db.getConnection();
try {
await conn.beginTransaction();
// Make sure every translation language exists in metadata table.
const ensured = new Set();
for (const t of (translations || [])) {
const code = String(t.languageCode || '').trim().toLowerCase();
if (!code || ensured.has(code)) continue;
await this.upsertLanguage(
conn,
{
languageCode: code,
label: code.toUpperCase(),
isEnabled: true,
isCustom: true,
},
updatedByUserId
);
ensured.add(code);
}
const result = await this.upsertTranslationOverrides(conn, translations || [], updatedByUserId);
await conn.commit();
return result;
} catch (error) {
await conn.rollback();
throw error;
} finally {
conn.release();
}
}
async getScanSummary({ languageCode } = {}) {
const code = languageCode ? String(languageCode).trim().toLowerCase() : null;
const [languages] = await db.query(
`SELECT language_code AS languageCode,
label,
is_enabled AS isEnabled,
is_custom AS isCustom
FROM i18n_languages
ORDER BY language_code`
);
const [namespaces] = await db.query(
`SELECT DISTINCT namespace
FROM i18n_translation_overrides
${code ? 'WHERE language_code = ?' : ''}
ORDER BY namespace`,
code ? [code] : []
);
const [countsByLanguage] = await db.query(
`SELECT language_code AS languageCode, COUNT(*) AS entryCount
FROM i18n_translation_overrides
${code ? 'WHERE language_code = ?' : ''}
GROUP BY language_code
ORDER BY language_code`,
code ? [code] : []
);
const prefs = await this.get();
const categoryNamespaces = Array.isArray(prefs.categories)
? [...new Set(prefs.categories.flatMap((c) => (Array.isArray(c?.namespaces) ? c.namespaces : [])))]
: [];
return {
languages: languages || [],
namespaces: (namespaces || []).map((r) => r.namespace),
countsByLanguage: countsByLanguage || [],
categories: prefs.categories || [],
globalKeys: prefs.globalKeys || [],
categoryNamespaces,
};
}
}
module.exports = I18nPreferencesRepository;

View File

@ -2,16 +2,204 @@ const db = require('../../database/database');
const { logger } = require('../../middleware/logger');
class CoffeeRepository {
async _syncSortOrders(coffeeId, conn) {
const cx = conn || db;
const [rows] = await cx.query(
`SELECT id
FROM coffee_table_images
WHERE coffee_id = ?
ORDER BY sort_order ASC, id ASC`,
[coffeeId]
);
for (let i = 0; i < (rows || []).length; i += 1) {
await cx.query('UPDATE coffee_table_images SET sort_order = ? WHERE id = ?', [i, rows[i].id]);
}
}
async ensureLegacyPictureRow(coffeeId, conn) {
const cx = conn || db;
const [coffeeRows] = await cx.query(
`SELECT object_storage_id, original_filename
FROM coffee_table
WHERE id = ?
LIMIT 1`,
[coffeeId]
);
const coffee = coffeeRows?.[0];
if (!coffee || !coffee.object_storage_id) return;
const [countRows] = await cx.query(
`SELECT COUNT(*) AS c
FROM coffee_table_images
WHERE coffee_id = ?`,
[coffeeId]
);
const count = Number(countRows?.[0]?.c || 0);
if (count > 0) return;
await cx.query(
`INSERT INTO coffee_table_images (coffee_id, object_storage_id, original_filename, sort_order)
VALUES (?, ?, ?, 0)`,
[coffeeId, coffee.object_storage_id, coffee.original_filename || null]
);
}
async listPicturesByCoffeeId(coffeeId, conn) {
const cx = conn || db;
const [rows] = await cx.query(
`SELECT id, coffee_id, object_storage_id, original_filename, sort_order, created_at
FROM coffee_table_images
WHERE coffee_id = ?
ORDER BY sort_order ASC, id ASC`,
[coffeeId]
);
return rows || [];
}
async deleteAllPicturesByCoffeeId(coffeeId, conn) {
const cx = conn || db;
const toDelete = await this.listPicturesByCoffeeId(coffeeId, cx);
if (!toDelete.length) return [];
await cx.query('DELETE FROM coffee_table_images WHERE coffee_id = ?', [coffeeId]);
return toDelete;
}
async deletePicturesByIds(coffeeId, pictureIds, conn) {
const cx = conn || db;
const ids = Array.isArray(pictureIds)
? pictureIds.map((x) => Number(x)).filter((x) => Number.isFinite(x) && x > 0)
: [];
if (!ids.length) return [];
const placeholders = ids.map(() => '?').join(',');
const [rows] = await cx.query(
`SELECT id, coffee_id, object_storage_id, original_filename, sort_order, created_at
FROM coffee_table_images
WHERE coffee_id = ?
AND id IN (${placeholders})`,
[coffeeId, ...ids]
);
if (!(rows || []).length) return [];
const deletePlaceholders = rows.map(() => '?').join(',');
await cx.query(
`DELETE FROM coffee_table_images
WHERE coffee_id = ?
AND id IN (${deletePlaceholders})`,
[coffeeId, ...rows.map((r) => r.id)]
);
return rows;
}
async addPictures(coffeeId, images, conn) {
const cx = conn || db;
const existing = await this.listPicturesByCoffeeId(coffeeId, cx);
let nextSort = existing.length;
for (const img of (images || [])) {
await cx.query(
`INSERT INTO coffee_table_images (coffee_id, object_storage_id, original_filename, sort_order)
VALUES (?, ?, ?, ?)`,
[
coffeeId,
img.object_storage_id,
img.original_filename || null,
nextSort,
]
);
nextSort += 1;
}
}
async syncPrimaryPictureFromGallery(coffeeId, conn) {
const cx = conn || db;
const [rows] = await cx.query(
`SELECT object_storage_id, original_filename
FROM coffee_table_images
WHERE coffee_id = ?
ORDER BY sort_order ASC, id ASC
LIMIT 1`,
[coffeeId]
);
const first = rows?.[0];
await cx.query(
`UPDATE coffee_table
SET object_storage_id = ?,
original_filename = ?,
updated_at = NOW()
WHERE id = ?`,
[first?.object_storage_id || null, first?.original_filename || null, coffeeId]
);
}
async _attachPictures(rows, conn) {
const cx = conn || db;
if (!Array.isArray(rows) || !rows.length) return rows || [];
const ids = rows.map((r) => Number(r.id)).filter((id) => Number.isFinite(id));
if (!ids.length) return rows;
const placeholders = ids.map(() => '?').join(',');
const [pictureRows] = await cx.query(
`SELECT id, coffee_id, object_storage_id, original_filename, sort_order, created_at
FROM coffee_table_images
WHERE coffee_id IN (${placeholders})
ORDER BY coffee_id ASC, sort_order ASC, id ASC`,
ids
);
const grouped = new Map();
for (const p of (pictureRows || [])) {
const key = Number(p.coffee_id);
if (!grouped.has(key)) grouped.set(key, []);
grouped.get(key).push({
id: Number(p.id),
coffee_id: Number(p.coffee_id),
object_storage_id: p.object_storage_id,
original_filename: p.original_filename,
sort_order: Number(p.sort_order || 0),
created_at: p.created_at,
});
}
for (const row of rows) {
const coffeeId = Number(row.id);
const fromTable = grouped.get(coffeeId) || [];
if (fromTable.length) {
row.pictures = fromTable;
} else if (row.object_storage_id) {
row.pictures = [{
id: null,
coffee_id: coffeeId,
object_storage_id: row.object_storage_id,
original_filename: row.original_filename || null,
sort_order: 0,
created_at: row.created_at || null,
}];
} else {
row.pictures = [];
}
}
return rows;
}
async listAll(conn) {
const cx = conn || db;
const [rows] = await cx.query('SELECT * FROM coffee_table ORDER BY id DESC');
return rows || [];
return this._attachPictures(rows || [], cx);
}
async getById(id, conn) {
const cx = conn || db;
const [rows] = await cx.query('SELECT * FROM coffee_table WHERE id = ? LIMIT 1', [id]);
return rows && rows[0] ? rows[0] : null;
if (!rows || !rows[0]) return null;
const hydrated = await this._attachPictures([rows[0]], cx);
return hydrated[0] || null;
}
async create(data, conn) {
@ -35,8 +223,32 @@ class CoffeeRepository {
data.state
];
const [result] = await cx.query(sql, params);
const createdId = result.insertId;
const pictures = Array.isArray(data.images) ? [...data.images] : [];
if (!pictures.length && data.object_storage_id) {
pictures.push({
object_storage_id: data.object_storage_id,
original_filename: data.original_filename || null,
sort_order: 0,
});
}
for (const picture of pictures) {
await cx.query(
`INSERT INTO coffee_table_images (coffee_id, object_storage_id, original_filename, sort_order)
VALUES (?, ?, ?, ?)`,
[
createdId,
picture.object_storage_id,
picture.original_filename || null,
Number.isFinite(Number(picture.sort_order)) ? Number(picture.sort_order) : 0,
]
);
}
logger.info('[CoffeeRepository.create] insert', { id: result.insertId });
return { id: result.insertId, ...data };
return this.getById(createdId, cx);
}
async update(id, data, conn) {
@ -80,7 +292,33 @@ class CoffeeRepository {
async listActive(conn) {
const cx = conn || db;
const [rows] = await cx.query('SELECT * FROM coffee_table WHERE state = TRUE ORDER BY id DESC');
return rows || [];
return this._attachPictures(rows || [], cx);
}
async editPictures(coffeeId, { replaceAll = false, removePictureIds = [], images = [] } = {}, conn) {
const cx = conn || db;
await this.ensureLegacyPictureRow(coffeeId, cx);
let deleted = [];
if (replaceAll) {
deleted = await this.deleteAllPicturesByCoffeeId(coffeeId, cx);
} else if (Array.isArray(removePictureIds) && removePictureIds.length) {
deleted = await this.deletePicturesByIds(coffeeId, removePictureIds, cx);
}
if (Array.isArray(images) && images.length) {
await this.addPictures(coffeeId, images, cx);
}
await this._syncSortOrders(coffeeId, cx);
await this.syncPrimaryPictureFromGallery(coffeeId, cx);
const updated = await this.getById(coffeeId, cx);
return {
updated,
deleted,
};
}
}

View File

@ -0,0 +1,207 @@
const db = require('../../database/database');
class MailTemplateRepository {
_mapRow(row) {
if (!row) return null;
return {
id: Number(row.id),
template_type: row.template_type,
name: row.name,
subject: row.subject,
html_content: row.html_content,
is_active: row.is_active === 1 || row.is_active === true,
is_archived: row.is_archived === 1 || row.is_archived === true,
archived_at: row.archived_at,
created_by: row.created_by,
updated_by: row.updated_by,
created_at: row.created_at,
updated_at: row.updated_at,
};
}
async list({ includeArchived = false, templateType } = {}) {
const where = [];
const params = [];
if (!includeArchived) {
where.push('is_archived = 0');
}
if (templateType) {
where.push('template_type = ?');
params.push(templateType);
}
const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
const [rows] = await db.query(
`SELECT *
FROM mail_templates
${whereClause}
ORDER BY template_type ASC, is_active DESC, updated_at DESC`,
params
);
return (rows || []).map((row) => this._mapRow(row));
}
async getById(id) {
const [rows] = await db.query(
`SELECT *
FROM mail_templates
WHERE id = ?
LIMIT 1`,
[id]
);
return this._mapRow(rows?.[0]);
}
async create(payload) {
const [result] = await db.query(
`INSERT INTO mail_templates
(template_type, name, subject, html_content, is_active, is_archived, created_by, updated_by)
VALUES (?, ?, ?, ?, 0, 0, ?, ?)`,
[
payload.template_type,
payload.name,
payload.subject || null,
payload.html_content,
payload.userId || null,
payload.userId || null,
]
);
return this.getById(result.insertId);
}
async update(id, payload) {
const fields = [];
const values = [];
if (payload.template_type !== undefined) {
fields.push('template_type = ?');
values.push(payload.template_type);
}
if (payload.name !== undefined) {
fields.push('name = ?');
values.push(payload.name);
}
if (payload.subject !== undefined) {
fields.push('subject = ?');
values.push(payload.subject || null);
}
if (payload.html_content !== undefined) {
fields.push('html_content = ?');
values.push(payload.html_content);
}
fields.push('updated_by = ?');
values.push(payload.userId || null);
if (!fields.length) return this.getById(id);
values.push(id);
const [result] = await db.query(
`UPDATE mail_templates
SET ${fields.join(', ')}, updated_at = NOW()
WHERE id = ?`,
values
);
if (!result?.affectedRows) return null;
return this.getById(id);
}
async activate(id, userId) {
const conn = await db.getConnection();
try {
await conn.beginTransaction();
const [targetRows] = await conn.query(
`SELECT id, template_type, is_archived
FROM mail_templates
WHERE id = ?
LIMIT 1
FOR UPDATE`,
[id]
);
const target = targetRows?.[0];
if (!target) {
await conn.rollback();
return null;
}
if (target.is_archived === 1 || target.is_archived === true) {
const error = new Error('Archived templates cannot be activated');
error.status = 400;
throw error;
}
await conn.query(
`UPDATE mail_templates
SET is_active = 0, updated_by = ?, updated_at = NOW()
WHERE template_type = ?`,
[userId || null, target.template_type]
);
await conn.query(
`UPDATE mail_templates
SET is_active = 1,
is_archived = 0,
archived_at = NULL,
updated_by = ?,
updated_at = NOW()
WHERE id = ?`,
[userId || null, id]
);
await conn.commit();
} catch (error) {
await conn.rollback();
throw error;
} finally {
conn.release();
}
return this.getById(id);
}
async archive(id, userId) {
const [result] = await db.query(
`UPDATE mail_templates
SET is_archived = 1,
is_active = 0,
archived_at = NOW(),
updated_by = ?,
updated_at = NOW()
WHERE id = ?`,
[userId || null, id]
);
if (!result?.affectedRows) return null;
return this.getById(id);
}
async unarchive(id, userId) {
const [result] = await db.query(
`UPDATE mail_templates
SET is_archived = 0,
archived_at = NULL,
updated_by = ?,
updated_at = NOW()
WHERE id = ?`,
[userId || null, id]
);
if (!result?.affectedRows) return null;
return this.getById(id);
}
async delete(id) {
const [result] = await db.query('DELETE FROM mail_templates WHERE id = ?', [id]);
return Number(result?.affectedRows || 0) > 0;
}
}
module.exports = new MailTemplateRepository();

View File

@ -146,6 +146,7 @@ class CompanyUserRepository {
branch,
numberOfEmployees,
registrationNumber,
atuNumber,
businessType,
iban,
accountHolderName,
@ -157,12 +158,13 @@ class CompanyUserRepository {
await conn.query(
`INSERT INTO company_profiles (
user_id, company_name, registration_number, phone, address, zip_code, city, country,
user_id, company_name, registration_number, atu_number, phone, address, zip_code, city, country,
branch, number_of_employees, business_type, contact_person_name, contact_person_phone, account_holder_name
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
company_name = VALUES(company_name),
registration_number = VALUES(registration_number),
atu_number = VALUES(atu_number),
phone = VALUES(phone),
address = VALUES(address),
zip_code = VALUES(zip_code),
@ -178,6 +180,7 @@ class CompanyUserRepository {
userId,
companyName,
registrationNumber || null,
atuNumber || null,
companyPhone || null,
address || null,
zip_code || null,

View File

@ -10,6 +10,8 @@ const CoffeeController = require('../controller/admin/CoffeeController');
const AffiliateController = require('../controller/affiliate/AffiliateController');
const NewsController = require('../controller/news/NewsController');
const PoolController = require('../controller/pool/PoolController');
const I18nPreferencesController = require('../controller/admin/I18nPreferencesController');
const MailTemplatesController = require('../controller/admin/MailTemplatesController');
// Helper middlewares for company-stamp
function forceCompanyForAdmin(req, res, next) {
@ -21,6 +23,7 @@ function forceCompanyForAdmin(req, res, next) {
// DELETE /admin/user/:id (moved from routes/admin.js)
router.delete('/admin/user/:id', authMiddleware, adminOnly, AdminUserController.deleteUser);
router.delete('/admin/mail-templates/:id', authMiddleware, adminOnly, MailTemplatesController.remove);
// DELETE /document-templates/:id (moved from routes/documentTemplates.js)
router.delete('/document-templates/:id', authMiddleware, DocumentTemplateController.deleteTemplate);
@ -37,5 +40,7 @@ router.delete('/admin/news/:id', authMiddleware, adminOnly, NewsController.delet
// Admin: remove pool members
router.delete('/admin/pools/:id/members', authMiddleware, adminOnly, PoolController.removeMembers);
router.delete('/admin/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.delete);
router.delete('/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.delete);
module.exports = router;

View File

@ -30,6 +30,11 @@ const DevManagementController = require('../controller/dev/DevManagementControll
const CompanySettingsController = require('../controller/admin/CompanySettingsController');
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
const ShippingFeesController = require('../controller/admin/ShippingFeesController');
const LoginController = require('../controller/login/LoginController');
const I18nPreferencesController = require('../controller/admin/I18nPreferencesController');
const MailTemplatesController = require('../controller/admin/MailTemplatesController');
const AUTH_VALIDATE_ROUTE_PATH = '/auth/validate';
// small helpers copied from original files
@ -44,6 +49,7 @@ function forceCompanyForAdmin(req, res, next) {
// === GET routes moved from other files ===
// auth.js GETs
router.get(AUTH_VALIDATE_ROUTE_PATH, LoginController.validate);
router.get('/me', authMiddleware, UserController.getMe);
router.get('/user/status', authMiddleware, UserStatusController.getStatus);
router.get('/user/status-progress', authMiddleware, UserStatusController.getStatusProgress);
@ -53,12 +59,18 @@ router.get('/users/:id/permissions', authMiddleware, PermissionController.getUse
router.get('/admin/users/:id/full', authMiddleware, adminOnly, AdminUserController.getFullUserAccountDetails);
router.get('/admin/users/:id/detailed', authMiddleware, adminOnly, AdminUserController.getDetailedUserInfo);
router.get('/admin/company-settings', authMiddleware, adminOnly, CompanySettingsController.get);
router.get('/admin/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.get);
router.get('/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.get);
router.get('/i18n/translations', authMiddleware, adminOnly, I18nPreferencesController.getTranslations);
router.get('/i18n/scan', authMiddleware, adminOnly, I18nPreferencesController.scan);
router.get('/users/:id/documents', authMiddleware, UserController.getUserDocumentsAndContracts);
router.get('/verify-password-reset', (req, res) => { /* Note: was moved from PasswordResetController.verifyPasswordResetToken */ res.status(204).end(); }); // keep placeholder if controller already registered via other verb
// admin.js GETs
router.get('/admin/user-stats', authMiddleware, adminOnly, AdminUserController.getUserStats);
router.get('/admin/user-list', authMiddleware, adminOnly, AdminUserController.getUserList);
router.get('/admin/mail-templates', authMiddleware, adminOnly, MailTemplatesController.list);
router.get('/admin/mail-templates/:id', authMiddleware, adminOnly, MailTemplatesController.getById);
router.get('/admin/verification-pending-users', authMiddleware, adminOnly, AdminUserController.getVerificationPendingUsers);
router.get('/admin/unverified-users', authMiddleware, adminOnly, AdminUserController.getUnverifiedUsers);
router.get('/admin/user/:id/documents', authMiddleware, adminOnly, UserDocumentController.getAllDocumentsForUser);
@ -131,7 +143,9 @@ router.get('/company-stamps/all', authMiddleware, adminOnly, forceCompanyForAdmi
// Admin: coffee products
router.get('/admin/coffee', authMiddleware, adminOnly, CoffeeController.list);
router.get('/admin/coffee/active', authMiddleware, adminOnly, CoffeeController.listActive);
router.get('/admin/coffee/:id/pictures', authMiddleware, adminOnly, CoffeeController.getPictures);
router.get('/coffee/active', authMiddleware, CoffeeController.listActive);
router.get('/coffee/:id/pictures', authMiddleware, CoffeeController.getPictures);
// Matrix GETs
@ -190,6 +204,7 @@ router.get('/news/:slug', NewsController.getPublic);
router.get('/invoices/mine', authMiddleware, InvoiceController.listMine);
router.get('/invoices/:id/pdf', authMiddleware, InvoiceController.downloadPdf);
router.get('/admin/invoices', authMiddleware, adminOnly, InvoiceController.adminList);
router.get('/admin/invoices/revenue-summary', authMiddleware, adminOnly, InvoiceController.revenueSummary);
router.get('/admin/invoices/:id/detail', authMiddleware, adminOnly, InvoiceController.getDetail);
// NOTE: Contract signing uses UnitOfWork; any DB cleanup must happen before commit() closes the connection.

View File

@ -15,6 +15,7 @@ const NewsController = require('../controller/news/NewsController');
const AbonemmentController = require('../controller/abonemments/AbonemmentController');
const InvoiceController = require('../controller/invoice/InvoiceController');
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
const MailTemplatesController = require('../controller/admin/MailTemplatesController');
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });
@ -36,11 +37,17 @@ router.patch('/company-stamps/:id/activate', authMiddleware, adminOnly, forceCom
// Admin user management PATCH routes
router.patch('/admin/archive-user/:id', authMiddleware, adminOnly, AdminUserController.archiveUser);
router.patch('/admin/unarchive-user/:id', authMiddleware, adminOnly, AdminUserController.unarchiveUser);
router.patch('/admin/mail-templates/:id', authMiddleware, adminOnly, MailTemplatesController.update);
router.patch('/admin/mail-templates/:id/activate', authMiddleware, adminOnly, MailTemplatesController.activate);
router.patch('/admin/mail-templates/:id/archive', authMiddleware, adminOnly, MailTemplatesController.archive);
router.patch('/admin/mail-templates/:id/unarchive', authMiddleware, adminOnly, MailTemplatesController.unarchive);
router.patch('/admin/update-verification/:id', authMiddleware, adminOnly, AdminUserController.updateUserVerification);
router.patch('/admin/update-user-profile/:id', authMiddleware, adminOnly, AdminUserController.updateUserProfile);
router.patch('/admin/update-user-status/:id', authMiddleware, adminOnly, AdminUserController.updateUserStatus);
// Admin: set state for coffee product
router.patch('/admin/coffee/:id/state', authMiddleware, adminOnly, CoffeeController.setState);
// Admin: edit coffee gallery pictures (upload/remove/replace)
router.patch('/admin/coffee/:id/pictures', authMiddleware, adminOnly, upload.any(), CoffeeController.editPictures);
// NEW: Admin pool active status update
router.patch('/admin/pools/:id/active', authMiddleware, adminOnly, PoolController.updateActive);
// NEW: Admin update pool linked subscription

View File

@ -32,6 +32,8 @@ const NewsController = require('../controller/news/NewsController');
const InvoiceController = require('../controller/invoice/InvoiceController'); // NEW
const DevManagementController = require('../controller/dev/DevManagementController');
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
const I18nPreferencesController = require('../controller/admin/I18nPreferencesController');
const MailTemplatesController = require('../controller/admin/MailTemplatesController');
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });
@ -82,6 +84,11 @@ router.post('/profile/company/complete', authMiddleware, CompanyProfileControlle
// Admin POSTs (moved from routes/admin.js)
router.post('/admin/verify-user/:id', authMiddleware, adminOnly, AdminUserController.verifyUser);
router.post('/admin/mail-templates', authMiddleware, adminOnly, MailTemplatesController.create);
router.post('/admin/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.post);
router.post('/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.post);
router.post('/i18n/translations', authMiddleware, adminOnly, I18nPreferencesController.upsertTranslations);
router.post('/i18n/scan', authMiddleware, adminOnly, I18nPreferencesController.scan);
router.post('/admin/send-password-reset/:userId', authMiddleware, adminOnly, async (req, res) => {
const userId = req.params.userId;
// require here to avoid circular/top-level ordering issues
@ -145,8 +152,8 @@ function ensureUserFromBody(req, res, next) {
// Company-stamp POST
router.post('/company-stamps', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.upload);
// Admin: create coffee product (supports multipart file 'picture')
router.post('/admin/coffee', authMiddleware, adminOnly, upload.single('picture'), CoffeeController.create);
// Admin: create coffee product (supports multipart files 'pictures' and legacy 'picture')
router.post('/admin/coffee', authMiddleware, adminOnly, upload.any(), CoffeeController.create);
// NEW: add user into matrix
router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser); // already added
// NEW: remove matrix user and create vacancy
@ -188,6 +195,7 @@ router.post('/abonements/referred', authMiddleware, ensureUserFromBody, Abonemme
// NEW: Invoice POSTs
router.post('/invoices/:id/pay', authMiddleware, adminOnly, InvoiceController.pay);
router.post('/admin/invoices/email-report', authMiddleware, adminOnly, InvoiceController.sendEmailReport);
router.post('/admin/invoices', authMiddleware, adminOnly, upload.single('pdf'), InvoiceController.adminCreate);
// Existing registration handlers (keep)
router.post('/register/personal', (req, res) => {
@ -205,4 +213,4 @@ router.post('/register/guest', (req, res) => {
console.log('✅ POST routes configured successfully');
module.exports = router;
module.exports = router;

View File

@ -9,6 +9,7 @@ const CoffeeController = require('../controller/admin/CoffeeController');
const CompanySettingsController = require('../controller/admin/CompanySettingsController');
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
const ShippingFeesController = require('../controller/admin/ShippingFeesController');
const I18nPreferencesController = require('../controller/admin/I18nPreferencesController');
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });
@ -28,5 +29,8 @@ router.put('/admin/dashboard-platforms/:id', authMiddleware, adminOnly, Dashboar
// Admin: update shipping fee for a piece count (60/120)
router.put('/admin/shipping-fees/:pieceCount', authMiddleware, adminOnly, ShippingFeesController.updatePrice);
router.put('/admin/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.put);
router.put('/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.put);
router.put('/i18n/translations', authMiddleware, adminOnly, I18nPreferencesController.upsertTranslations);
module.exports = router;

View File

@ -2,7 +2,7 @@ const UnitOfWork = require('../database/UnitOfWork');
const argon2 = require('argon2');
async function createCompanyUser() {
return
// Edit these values directly in code (no env vars)
const companyEmail = 'dummy-company@profitplanet.local';
const companyPassword = 'dummyPass!1234';

View File

@ -7,6 +7,22 @@ const ReferralService = require('../referral/ReferralService');
const ReferralTokenRepository = require('../../repositories/referral/ReferralTokenRepository');
const MailService = require('../email/MailService');
const CAPSULES_PER_PACK = 10;
const MIN_ABO_PACKS = 6;
const MAX_ABO_PACKS = 10000;
function getPackCountError(totalPacks) {
if (totalPacks < MIN_ABO_PACKS) {
return `Order must contain at least ${MIN_ABO_PACKS} packs (${MIN_ABO_PACKS * CAPSULES_PER_PACK} capsules).`;
}
if (totalPacks > MAX_ABO_PACKS) {
return `Order cannot contain more than ${MAX_ABO_PACKS} packs (${MAX_ABO_PACKS * CAPSULES_PER_PACK} capsules).`;
}
return null;
}
class AbonemmentService {
constructor() {
this.repo = new AbonemmentRepository();
@ -40,7 +56,7 @@ class AbonemmentService {
return typeof email === 'string' ? email.trim().toLowerCase() : null;
}
// NEW: single bundle subscribe using items array (12 packs, 120 capsules)
// NEW: single bundle subscribe using items array (pack-based order content)
async subscribeOrder({
items,
billingInterval,
@ -114,7 +130,7 @@ class AbonemmentService {
const coffeeId = item?.coffeeId;
const packs = Number(item?.quantity ?? 0);
if (!coffeeId) throw new Error('coffeeId is required for each item');
if (!Number.isFinite(packs) || packs <= 0) throw new Error('quantity must be a positive integer per item');
if (!Number.isInteger(packs) || packs <= 0) throw new Error('quantity must be a positive integer per item');
const product = await this.getCoffeeProduct(coffeeId);
if (!product || !product.is_active) throw new Error(`Product ${coffeeId} not available`);
@ -134,6 +150,9 @@ class AbonemmentService {
});
}
const packCountError = getPackCountError(totalPacks);
if (packCountError) throw new Error(packCountError);
const now = new Date();
const nextBilling = this.addInterval(now, billingInterval || 'month', intervalCount || 1);
@ -590,7 +609,7 @@ class AbonemmentService {
const coffeeId = item?.coffeeId;
const packs = Number(item?.quantity ?? 0);
if (!coffeeId) throw new Error('coffeeId is required for each item');
if (!Number.isFinite(packs) || packs <= 0) {
if (!Number.isInteger(packs) || packs <= 0) {
throw new Error('quantity must be a positive integer per item');
}
@ -611,9 +630,8 @@ class AbonemmentService {
});
}
if (totalPacks !== 6 && totalPacks !== 12) {
throw new Error('Order must contain exactly 6 packs (60 capsules) or 12 packs (120 capsules).');
}
const packCountError = getPackCountError(totalPacks);
if (packCountError) throw new Error(packCountError);
const previousPacks = Array.isArray(abon.pack_breakdown)
? abon.pack_breakdown.reduce((sum, item) => sum + Number(item?.packs || item?.quantity || 0), 0)

View File

@ -1,6 +1,5 @@
const InvoiceRepository = require('../../repositories/invoice/InvoiceRepository');
const UnitOfWork = require('../../database/UnitOfWork'); // NEW
const TaxRepository = require('../../repositories/tax/taxRepository'); // NEW
const PoolInflowService = require('../pool/PoolInflowService');
const DocumentTemplateService = require('../template/DocumentTemplateService');
const MailService = require('../email/MailService');
@ -10,8 +9,8 @@ const { logger } = require('../../middleware/logger');
const puppeteer = require('puppeteer');
const fs = require('fs/promises');
const path = require('path');
const pool = require('../../database/database');
const CompanySettingsRepository = require('../../repositories/settings/CompanySettingsRepository');
const CoffeeShippingFeeService = require('../subscriptions/CoffeeShippingFeeService');
class InvoiceService {
@ -19,15 +18,6 @@ class InvoiceService {
this.repo = new InvoiceRepository();
}
_inferImageMimeFromBase64(base64) {
const s = String(base64 || '').trim();
if (!s) return 'image/png';
if (s.startsWith('iVBORw0KGgo')) return 'image/png';
if (s.startsWith('/9j/')) return 'image/jpeg';
if (s.startsWith('R0lGOD')) return 'image/gif';
return 'image/png';
}
_templateHasVars(template, varNames) {
if (!template) return false;
return varNames.every((name) => {
@ -47,15 +37,19 @@ class InvoiceService {
}
_resolvePieceCountForQr(abonement) {
const breakdown = Array.isArray(abonement?.pack_breakdown) ? abonement.pack_breakdown : [];
const totalPacks = breakdown.reduce((sum, item) => sum + Number(item?.packs || item?.quantity || 0), 0);
const piecesByPack = totalPacks ? totalPacks * 10 : null;
if (piecesByPack != null) {
if (piecesByPack >= 120) return 120;
if (piecesByPack >= 60) return 60;
return null;
}
const packGroup = String(abonement?.pack_group || '').toLowerCase();
if (packGroup.includes('120')) return 120;
if (packGroup.includes('60')) return 60;
const breakdown = Array.isArray(abonement?.pack_breakdown) ? abonement.pack_breakdown : [];
const totalPacks = breakdown.reduce((sum, item) => sum + Number(item?.packs || 0), 0);
const piecesByPack = totalPacks ? totalPacks * 10 : null;
if (piecesByPack === 60 || piecesByPack === 120) return piecesByPack;
return null;
}
@ -107,35 +101,8 @@ class InvoiceService {
return items;
}
async _getCompanySettingsQrDataUri(pieceCount) {
const safePieceCount = pieceCount === 120 ? 120 : 60;
try {
const repo = new CompanySettingsRepository();
const row = await repo.get();
if (!row) return null;
const raw = safePieceCount === 120 ? row?.qr_code_120_base64 : row?.qr_code_60_base64;
const value = (raw == null) ? '' : String(raw).trim();
if (!value) return null;
if (value.startsWith('data:image/')) return value;
const mime = this._inferImageMimeFromBase64(value);
return `data:${mime};base64,${value}`;
} catch (e) {
logger.warn('InvoiceService._getCompanySettingsQrDataUri:error', {
pieceCount: safePieceCount,
message: e?.message,
});
return null;
}
}
async _buildQrCodeImageTag({ abonement }) {
const pieceCount = this._resolvePieceCountForQr(abonement);
if (!pieceCount) return '';
const dataUri = await this._getCompanySettingsQrDataUri(pieceCount);
if (!dataUri) return '';
return `<img alt="QR Code" src="${this._escapeHtml(dataUri)}" />`;
return '';
}
_escapeHtml(value) {
@ -310,13 +277,37 @@ class InvoiceService {
return `<strong>${this._escapeHtml(bankAccountHolder)}</strong><br>${this._escapeHtml(bankIban)}<br>${this._escapeHtml(bankBic)}`;
}
async _loadCompanyInfo() {
try {
const [rows] = await pool.query(
`SELECT company_name, company_street, company_postal_city, company_country,
company_logo_base64, company_logo_mime_type
FROM company_settings
WHERE id = 1
LIMIT 1`
);
return rows?.[0] || {};
} catch (e) {
logger.warn('InvoiceService._loadCompanyInfo:error', { message: e?.message });
return {};
}
}
_buildCompanyLogoTag(companyInfo) {
const base64 = typeof companyInfo?.company_logo_base64 === 'string' ? companyInfo.company_logo_base64.trim() : '';
const mimeType = typeof companyInfo?.company_logo_mime_type === 'string' ? companyInfo.company_logo_mime_type.trim() : '';
if (!base64 || !mimeType || !mimeType.startsWith('image/')) return '';
const alt = this._escapeHtml(companyInfo?.company_name || 'Company logo');
return `<img src="data:${mimeType};base64,${base64}" alt="${alt}">`;
}
_prepareVariablesForTemplate(templateHtml, variables) {
// Ensure backwards compatibility with older templates that only contain {{paymentInfoText}}
// by injecting the Profit Planet bank block (and optionally QR) into paymentInfoText.
// by injecting the Profit Planet bank block into paymentInfoText.
if (!templateHtml) return variables;
const supportsBankVars = this._templateHasVars(templateHtml, ['bankAccountHolder', 'bankIban', 'bankBic']);
const supportsQrVar = this._templateHasVars(templateHtml, ['qrCodeImage']);
const bankBlock = this._getProfitPlanetBankBlockHtml({
bankAccountHolder: variables.bankAccountHolder || 'Profit Planet GmbH',
@ -330,11 +321,6 @@ class InvoiceService {
next.paymentInfoText = bankBlock;
}
if (!supportsQrVar && variables.qrCodeImage) {
// Append QR under payment info text when there's no dedicated placeholder
next.paymentInfoText = `${next.paymentInfoText || ''}<br><br>${variables.qrCodeImage}`;
}
return next;
}
@ -344,6 +330,8 @@ class InvoiceService {
const issuedAt = invoice.issued_at ? new Date(invoice.issued_at).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);
const dueAt = invoice.due_at ? new Date(invoice.due_at).toISOString().slice(0, 10) : '-';
const vatRate = invoice.vat_rate != null ? Number(invoice.vat_rate) : 0;
const taxMode = String(invoice?.context?.tax_mode || 'standard').toLowerCase();
const isReverseCharge = taxMode === 'reverse_charge';
// Hardcoded bank info (Profit Planet)
const bankAccountHolder = 'Profit Planet GmbH';
@ -365,12 +353,14 @@ class InvoiceService {
bankBic,
].join('<br>');
// Hardcoded company address (Profit Planet)
const storedCompanyInfo = await this._loadCompanyInfo();
const companyInfo = {
company_name: 'Profit Planet GmbH',
company_street: 'Kärntner Straße 227',
company_postal_city: '8053 Graz',
company_country: '',
company_name: storedCompanyInfo.company_name || 'Profit Planet GmbH',
company_street: storedCompanyInfo.company_street || 'Kärntner Straße 227',
company_postal_city: storedCompanyInfo.company_postal_city || '8053 Graz',
company_country: storedCompanyInfo.company_country || 'Austria',
company_logo_base64: storedCompanyInfo.company_logo_base64 || null,
company_logo_mime_type: storedCompanyInfo.company_logo_mime_type || null,
};
// For gift subscriptions: "Bill To" = recipient, "Ordered by" = purchaser
@ -415,6 +405,7 @@ class InvoiceService {
companyStreet: this._escapeHtml(companyInfo.company_street || ''),
companyPostalCity: this._escapeHtml(companyInfo.company_postal_city || ''),
companyCountry: this._escapeHtml(companyInfo.company_country || 'Germany'),
companyLogo: this._buildCompanyLogoTag(companyInfo),
customerName: this._escapeHtml(customerName),
customerEmail: this._escapeHtml(customerEmail),
customerStreet: this._escapeHtml(invoice.buyer_street || ''),
@ -429,16 +420,20 @@ class InvoiceService {
totalHeader: isDe ? 'Gesamt' : 'Total',
itemsRows: this._buildItemsTableRows(items, invoice.currency),
subtotalLabel: isDe ? 'Nettobetrag' : 'Subtotal (net)',
taxLabel: isDe ? 'MwSt.' : 'Tax',
vatRateDisplay: vatRate ? `${vatRate}%` : '0%',
taxLabel: isReverseCharge ? (isDe ? 'Reverse Charge' : 'Reverse charge') : (isDe ? 'MwSt.' : 'Tax'),
vatRateDisplay: isReverseCharge ? (isDe ? 'nicht ausgewiesen' : 'not charged') : (vatRate ? `${vatRate}%` : '0%'),
totalNet: this._escapeHtml(this._formatAmount(invoice.total_net, invoice.currency)),
totalTax: this._escapeHtml(this._formatAmount(invoice.total_tax, invoice.currency)),
totalLabel: isDe ? 'Gesamtbetrag (brutto)' : 'Total (gross)',
totalGross: this._escapeHtml(this._formatAmount(invoice.total_gross, invoice.currency)),
paymentInfoTitle: isDe ? 'Zahlungsinformationen' : 'Payment Information',
paymentInfoText: isDe
? 'Bitte überweisen Sie den Gesamtbetrag unter Angabe der Rechnungsnummer.'
: 'Please transfer the total amount stating the invoice number as reference.',
? (isReverseCharge
? 'Reverse-Charge-Verfahren: Steuerschuldnerschaft des Leistungsempfängers. Bitte überweisen Sie den Gesamtbetrag unter Angabe der Rechnungsnummer.'
: 'Bitte überweisen Sie den Gesamtbetrag unter Angabe der Rechnungsnummer.')
: (isReverseCharge
? 'Reverse charge applies: VAT liability shifts to the recipient. Please transfer the total amount stating the invoice number as reference.'
: 'Please transfer the total amount stating the invoice number as reference.'),
bankAccountHolder: this._escapeHtml(bankAccountHolder),
bankIban: this._escapeHtml(bankIban),
bankBic: this._escapeHtml(bankBic),
@ -553,29 +548,14 @@ class InvoiceService {
if (templateHtml) {
const supportsBankVars = this._templateHasVars(templateHtml, ['bankAccountHolder', 'bankIban', 'bankBic']);
const supportsQrVar = this._templateHasVars(templateHtml, ['qrCodeImage']);
const pieceCountForQr = this._resolvePieceCountForQr(abonement);
logger.info('InvoiceService._sendInvoiceEmail:template_compat', {
invoiceId: invoice?.id,
lang,
supportsBankVars,
supportsQrVar,
pieceCountForQr,
hasQrImage: Boolean(variables?.qrCodeImage),
});
const varsForTemplate = this._prepareVariablesForTemplate(templateHtml, variables);
html = this._renderTemplate(templateHtml, varsForTemplate);
// Final guard: if we still didn't embed QR but we expected one, force local template
const missingQr = variables.qrCodeImage && !html.includes('data:image/png;base64,');
if (missingQr) {
const localTemplate = await this._loadLocalInvoiceTemplateHtml();
if (localTemplate) {
const varsForLocal = this._prepareVariablesForTemplate(localTemplate, variables);
html = this._renderTemplate(localTemplate, varsForLocal);
}
}
}
const htmlForPdf = html || await this._buildFallbackInvoiceHtml({ invoice, items, abonement, lang });
@ -598,31 +578,92 @@ class InvoiceService {
}
// NEW: resolve current standard VAT rate for a buyer country code
async resolveVatRateForCountry(countryCode) {
if (!countryCode) return null;
async _resolveCountryByInput(conn, countryInput) {
const raw = String(countryInput || '').trim();
if (!raw) return null;
const code = raw.toUpperCase();
const [byCode] = await conn.query(
`SELECT id, country_code, country_name FROM countries WHERE UPPER(country_code) = ? LIMIT 1`,
[code],
);
if (byCode?.[0]) return byCode[0];
const [byName] = await conn.query(
`SELECT id, country_code, country_name FROM countries WHERE LOWER(country_name) = LOWER(?) LIMIT 1`,
[raw],
);
return byName?.[0] || null;
}
_normalizeUid(value) {
return String(value || '').trim().toUpperCase().replace(/[^A-Z0-9]/g, '');
}
_isLikelyValidUid(value) {
const uid = this._normalizeUid(value);
return /^[A-Z]{2}[A-Z0-9]{6,14}$/.test(uid);
}
async _loadCompanyTaxProfile(userId) {
if (!userId) return null;
const [rows] = await pool.query(
`SELECT registration_number, atu_number, country
FROM company_profiles
WHERE user_id = ?
LIMIT 1`,
[userId],
);
return rows?.[0] || null;
}
async resolveTaxDecisionForSubscription({ buyerCountry, invoiceOwnerUserId }) {
const uow = new UnitOfWork();
await uow.start();
const taxRepo = new TaxRepository(uow);
try {
const country = await taxRepo.getCountryByCode(String(countryCode).toUpperCase());
if (!country) {
await uow.commit();
return null;
const country = await this._resolveCountryByInput(uow.getConnection(), buyerCountry);
let vatRate = null;
if (country?.id) {
const [rows] = await uow.getConnection().query(
`SELECT standard_rate FROM vat_rates WHERE country_id = ? AND effective_to IS NULL LIMIT 1`,
[country.id],
);
vatRate = rows?.[0]?.standard_rate == null ? null : Number(rows[0].standard_rate);
}
// get current vat row for this country
const [rows] = await taxRepo.conn.query(
`SELECT standard_rate FROM vat_rates WHERE country_id = ? AND effective_to IS NULL LIMIT 1`,
[country.id]
);
await uow.commit();
const rate = rows?.[0]?.standard_rate;
return rate == null ? null : Number(rate);
const companyProfile = await this._loadCompanyTaxProfile(invoiceOwnerUserId);
const uidCandidate = companyProfile?.atu_number || companyProfile?.registration_number || '';
const normalizedUid = this._normalizeUid(uidCandidate);
const hasValidUid = this._isLikelyValidUid(normalizedUid);
const countryCode = String(country?.country_code || '').toUpperCase();
// Reverse charge for company customers with a valid UID outside seller country (AT).
const isReverseCharge = Boolean(hasValidUid && countryCode && countryCode !== 'AT');
return {
vatRate: isReverseCharge ? 0 : vatRate,
isReverseCharge,
countryCode: countryCode || null,
uid: hasValidUid ? normalizedUid : null,
};
} catch (e) {
await uow.rollback();
throw e;
}
}
async resolveVatRateForCountry(countryCode) {
const decision = await this.resolveTaxDecisionForSubscription({
buyerCountry: countryCode,
invoiceOwnerUserId: null,
});
return decision?.vatRate ?? null;
}
// Issue invoice for a subscription period, with items from pack_breakdown
async issueForAbonement(abonement, periodStart, periodEnd, { actorUserId, lang = 'en' } = {}) {
console.log('[INVOICE ISSUE] Inputs:', {
@ -644,8 +685,12 @@ class InvoiceService {
};
const currency = abonement.currency || 'EUR';
// NEW: resolve invoice vat_rate (standard) from buyer country
const vat_rate = await this.resolveVatRateForCountry(addr.country);
// CHANGED: resolve tax mode for this subscription (standard VAT vs reverse charge)
const taxDecision = await this.resolveTaxDecisionForSubscription({
buyerCountry: addr.country,
invoiceOwnerUserId: actorUserId ?? abonement.user_id ?? abonement.purchaser_user_id ?? null,
});
const vat_rate = taxDecision?.vatRate ?? null;
const items = await this._buildInvoiceItems({ abonement, vatRate: vat_rate, lang });
@ -655,6 +700,9 @@ class InvoiceService {
period_start: periodStart,
period_end: periodEnd,
referred_by: abonement.referred_by || null,
tax_mode: taxDecision?.isReverseCharge ? 'reverse_charge' : 'standard',
customer_country_code: taxDecision?.countryCode || null,
uid_number: taxDecision?.uid || null,
};
// CHANGED: prioritize token user id for invoice ownership
@ -743,18 +791,34 @@ class InvoiceService {
return paidInvoice;
}
async syncOverdueStatuses() {
return this.repo.markIssuedPastDueAsOverdue();
}
async listMine(userId, { status, limit = 50, offset = 0 } = {}) {
await this.syncOverdueStatuses();
return this.repo.listByUser(userId, { status, limit, offset });
}
async listByAbonement(abonementId) {
await this.syncOverdueStatuses();
return this.repo.findByAbonement(abonementId);
}
async adminList({ status, limit = 200, offset = 0 } = {}) {
await this.syncOverdueStatuses();
return this.repo.listAll({ status, limit, offset });
}
async getRevenueSummary() {
const summary = await this.repo.getPaidRevenueSummary();
return {
totalPaidAllTime: Number(summary?.total_paid_all_time || 0),
currency: summary?.currency || 'EUR',
paidInvoiceCount: Number(summary?.paid_invoice_count || 0),
};
}
async updateStatus(invoiceId, newStatus) {
const invoice = await this.repo.getById(invoiceId);
if (!invoice) throw new Error(`Invoice ${invoiceId} not found.`);
@ -772,6 +836,7 @@ class InvoiceService {
}
async getInvoiceDetail(invoiceId) {
await this.syncOverdueStatuses();
const invoice = await this.repo.getById(invoiceId);
if (!invoice) throw new Error(`Invoice ${invoiceId} not found.`);
const items = await this.repo.getItemsByInvoiceId(invoiceId);
@ -949,6 +1014,42 @@ class InvoiceService {
return { sentCount: paidInvoices.length };
}
async adminCreateManual(fields, pdfBuffer = null) {
const { uploadBuffer } = require('../../utils/exoscaleUploader');
const invoice = await this.repo.createManualInvoice({
source_type: 'manual',
buyer_name: fields.buyer_name || null,
buyer_email: fields.buyer_email || null,
buyer_street: fields.buyer_street || null,
buyer_postal_code: fields.buyer_postal_code || null,
buyer_city: fields.buyer_city || null,
buyer_country: fields.buyer_country || null,
currency: fields.currency || 'EUR',
total_net: fields.total_net != null ? Number(fields.total_net) : 0,
total_tax: fields.total_tax != null ? Number(fields.total_tax) : 0,
total_gross: Number(fields.total_gross || 0),
vat_rate: fields.vat_rate != null ? Number(fields.vat_rate) : null,
status: fields.status || 'issued',
issued_at: fields.issued_at ? new Date(fields.issued_at) : new Date(),
due_at: fields.due_at ? new Date(fields.due_at) : null,
context: { source: 'admin_manual_upload' },
});
if (pdfBuffer && pdfBuffer.length > 0) {
const { objectKey } = await uploadBuffer(
pdfBuffer,
`invoice-${invoice.invoice_number}.pdf`,
'application/pdf',
`invoices/admin/${invoice.id}`,
);
await this.repo.updateStorageKey(invoice.id, objectKey);
return this.repo.getById(invoice.id);
}
return invoice;
}
}
module.exports = InvoiceService;

View File

@ -221,6 +221,63 @@ class LoginService {
throw error;
}
}
static async validateByRefreshToken(refreshToken) {
logger.info('LoginService.validateByRefreshToken:start');
const unitOfWork = new UnitOfWork();
await unitOfWork.start();
unitOfWork.registerRepository('login', new LoginRepository(unitOfWork));
unitOfWork.registerRepository('user', new UserRepository(unitOfWork));
unitOfWork.registerRepository('status', new UserStatusRepository(unitOfWork));
try {
const loginRepo = unitOfWork.getRepository('login');
const tokenRecord = await loginRepo.findRefreshToken(refreshToken);
if (!tokenRecord) {
const error = new Error('Invalid or expired refresh token');
error.status = 401;
throw error;
}
if (new Date(tokenRecord.expires_at) < new Date()) {
const error = new Error('Refresh token expired');
error.status = 401;
throw error;
}
const userRepo = unitOfWork.getRepository('user');
const user = await userRepo.findUserByEmailOrId(tokenRecord.user_id);
if (!user) {
const error = new Error('User not found');
error.status = 401;
throw error;
}
const statusRepo = unitOfWork.getRepository('status');
const userStatus = await statusRepo.getStatusByUserId(user.id);
if (userStatus && userStatus.status === 'suspended') {
const error = new Error('Account suspended');
error.status = 403;
throw error;
}
const role = await loginRepo.getUserRole(user.id);
const permissions = await loginRepo.getUserPermissions(user.id);
await unitOfWork.commit();
return {
user: {
...user.getPublicData(),
role,
permissions,
},
};
} catch (error) {
await unitOfWork.rollback(error);
throw error;
}
}
}
// Helper for finding user by id or email

View File

@ -13,7 +13,6 @@ class CompanyProfileService {
{ key: 'zip_code', label: 'Postal code' },
{ key: 'city', label: 'City' },
{ key: 'country', label: 'Country' },
{ key: 'registrationNumber', label: 'VAT' },
{ key: 'accountHolderName', label: 'Account holder' },
{ key: 'iban', label: 'IBAN' }
];
@ -26,6 +25,8 @@ class CompanyProfileService {
}
profileData.companyName = (profileData.companyName || '').toString().trim();
profileData.registrationNumber = (profileData.registrationNumber || '').toString().trim() || null;
profileData.atuNumber = (profileData.atuNumber || profileData.uidNumber || '').toString().trim() || null;
// Pass all profileData including country to repository
const repo = new CompanyUserRepository(unitOfWork);

View File

@ -99,6 +99,30 @@ class CoffeeService {
async listActive() {
return CoffeeRepository.listActive();
}
async editPictures(id, payload = {}) {
const numericId = Number(id);
if (!Number.isFinite(numericId) || numericId <= 0) {
throw new Error('Invalid coffee id');
}
const uow = new UnitOfWork();
try {
await uow.start();
const existing = await CoffeeRepository.getById(numericId, uow.connection);
if (!existing) {
await uow.rollback();
return null;
}
const result = await CoffeeRepository.editPictures(numericId, payload, uow.connection);
await uow.commit();
return result;
} catch (e) {
try { await uow.rollback(e); } catch(_) {}
throw e;
}
}
}
module.exports = new CoffeeService();

View File

@ -0,0 +1,133 @@
const MailTemplateRepository = require('../../repositories/template/MailTemplateRepository');
class MailTemplateService {
_requiredString(value, fieldName) {
const parsed = String(value == null ? '' : value).trim();
if (!parsed) {
const error = new Error(`${fieldName} is required`);
error.status = 400;
throw error;
}
return parsed;
}
_optionalString(value) {
if (value === undefined || value === null) return null;
const parsed = String(value).trim();
return parsed || null;
}
_toBool(value, fallback = false) {
if (value === undefined || value === null) return fallback;
if (typeof value === 'boolean') return value;
const normalized = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return fallback;
}
async list(query = {}) {
return MailTemplateRepository.list({
includeArchived: this._toBool(query.includeArchived, false),
templateType: this._optionalString(query.templateType),
});
}
async getById(id) {
const numericId = Number(id);
if (!Number.isFinite(numericId) || numericId <= 0) {
const error = new Error('Invalid id');
error.status = 400;
throw error;
}
return MailTemplateRepository.getById(numericId);
}
async create(payload = {}, userId = null) {
const template_type = this._requiredString(payload.template_type, 'template_type');
const name = this._requiredString(payload.name, 'name');
const html_content = this._requiredString(payload.html_content, 'html_content');
return MailTemplateRepository.create({
template_type,
name,
subject: this._optionalString(payload.subject),
html_content,
userId,
});
}
async update(id, payload = {}, userId = null) {
const numericId = Number(id);
if (!Number.isFinite(numericId) || numericId <= 0) {
const error = new Error('Invalid id');
error.status = 400;
throw error;
}
const updatePayload = {};
if (Object.prototype.hasOwnProperty.call(payload, 'template_type')) {
updatePayload.template_type = this._requiredString(payload.template_type, 'template_type');
}
if (Object.prototype.hasOwnProperty.call(payload, 'name')) {
updatePayload.name = this._requiredString(payload.name, 'name');
}
if (Object.prototype.hasOwnProperty.call(payload, 'subject')) {
updatePayload.subject = this._optionalString(payload.subject);
}
if (Object.prototype.hasOwnProperty.call(payload, 'html_content')) {
updatePayload.html_content = this._requiredString(payload.html_content, 'html_content');
}
updatePayload.userId = userId;
return MailTemplateRepository.update(numericId, updatePayload);
}
async activate(id, userId = null) {
const numericId = Number(id);
if (!Number.isFinite(numericId) || numericId <= 0) {
const error = new Error('Invalid id');
error.status = 400;
throw error;
}
return MailTemplateRepository.activate(numericId, userId);
}
async archive(id, userId = null) {
const numericId = Number(id);
if (!Number.isFinite(numericId) || numericId <= 0) {
const error = new Error('Invalid id');
error.status = 400;
throw error;
}
return MailTemplateRepository.archive(numericId, userId);
}
async unarchive(id, userId = null) {
const numericId = Number(id);
if (!Number.isFinite(numericId) || numericId <= 0) {
const error = new Error('Invalid id');
error.status = 400;
throw error;
}
return MailTemplateRepository.unarchive(numericId, userId);
}
async remove(id) {
const numericId = Number(id);
if (!Number.isFinite(numericId) || numericId <= 0) {
const error = new Error('Invalid id');
error.status = 400;
throw error;
}
return MailTemplateRepository.delete(numericId);
}
}
module.exports = new MailTemplateService();

View File

@ -344,8 +344,8 @@
</div>
<div class="company">
<div><strong>PROFIT PLANET GMBH</strong></div>
<div>Liebenauer Hauptstraße 82c</div>
<div>A-8041 Graz</div>
<div>Kärntner Straße 227</div>
<div>8053 Graz</div>
<div class="muted">FN-649474 i</div>
<div class="muted" style="margin-top:6px;">IBAN: AT16 2081 5000 4639 9507</div>
<div class="muted">Swift/BIC Code: STSPAT2GXXX</div>

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

View File

@ -0,0 +1,737 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>abo-contract-DE</title>
<style>
@page { size: A4; margin: 14mm; }
:root {
--ink: #111827;
--muted: #6b7280;
--line: #d1d5db;
--soft: #f3f4f6;
--soft2: #f9fafb;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
color: var(--ink);
font-family: Arial, Helvetica, sans-serif;
font-size: 11.5pt;
line-height: 1.35;
background: #ffffff;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
body { padding: 0; }
p, li, td, th {
white-space: pre-wrap;
}
.document {
width: 100%;
max-width: 860px;
margin: 0 auto;
padding: 0 16px 18px;
}
.header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
padding: 18px 0 10px;
border-bottom: 2px solid var(--ink);
margin-bottom: 12px;
}
.brand .title {
font-weight: 800;
letter-spacing: 0.2px;
font-size: 16pt;
margin: 0 0 4px;
}
.brand .subtitle {
margin: 0;
color: var(--muted);
font-size: 10pt;
}
.company {
text-align: right;
font-size: 9.5pt;
max-width: 340px;
}
.metaGrid {
display: grid;
grid-template-columns: auto auto;
justify-content: end;
align-items: end;
column-gap: 8px;
row-gap: 4px;
text-align: right;
font-size: 10pt;
color: var(--muted);
}
h1 {
margin: 10px 0 0;
font-size: 15pt;
letter-spacing: 0.2px;
}
h2 {
margin: 18px 0 8px;
font-size: 12.5pt;
border-bottom: 1px solid var(--line);
padding-bottom: 4px;
}
.box {
border: 1px solid var(--line);
border-radius: 8px;
padding: 10px;
background: var(--soft2);
margin: 10px 0;
}
.grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.row {
display: grid;
grid-template-columns: 180px 1fr;
gap: 8px;
padding: 4px 0;
border-bottom: 1px dashed #e5e7eb;
}
.row:last-child { border-bottom: 0; }
.label {
color: var(--muted);
font-size: 10pt;
}
.value {
font-weight: 600;
}
.fill {
display: inline-block;
min-width: 160px;
border-bottom: 1px solid #9ca3af;
padding: 0 4px 1px;
font-weight: 600;
}
.fill.wide { min-width: 280px; }
.fill.full { display: inline-block; min-width: 100%; }
.checkline {
display: flex;
gap: 18px;
flex-wrap: wrap;
align-items: center;
padding: 6px 0;
}
.check {
display: inline-flex;
gap: 8px;
align-items: center;
font-size: 10.5pt;
}
.checkbox {
width: 14px;
height: 14px;
border: 1px solid var(--ink);
display: inline-block;
border-radius: 2px;
position: relative;
flex: 0 0 auto;
}
.checkbox.checked::after {
content: "";
position: absolute;
left: 3px;
top: 0;
width: 6px;
height: 10px;
border-right: 2px solid var(--ink);
border-bottom: 2px solid var(--ink);
transform: rotate(40deg);
}
table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--line);
margin: 14px 0;
background: #fff;
}
th, td {
border: 1px solid var(--line);
padding: 8px 10px;
vertical-align: top;
text-align: left;
}
th {
background: var(--soft);
}
.signature-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin: 14px 0 10px;
}
.signature-box {
border-top: 1px solid #111827;
padding-top: 8px;
min-height: 130px;
text-align: center;
color: var(--muted);
font-size: 10pt;
}
.pp-stamp,
.pp-stamp img,
.signature-box img {
max-width: 280px;
max-height: 120px;
width: auto;
height: auto;
display: block;
margin: 0 auto 8px;
}
.muted { color: var(--muted); }
@media print {
.header,
.box,
.grid2,
table,
tr,
.signature-grid,
.signature-box {
break-inside: avoid;
page-break-inside: avoid;
}
}
</style>
</head>
<body>
<div class="document">
<div class="header">
<div class="brand">
<p class="title">ABO Vertrag</p>
<p class="subtitle">Angebot auf Abschluss eines Kauf- Mietvertrages Kaffee-Service- Kapsel</p>
</div>
<div class="company">
<div><strong>PROFIT PLANET GMBH</strong></div>
<div>Kärntner Straße 227</div>
<div>A-8053 Graz</div>
<div class="muted">FN-649474 i</div>
<div class="muted" style="margin-top:6px;">IBAN: AT16 2081 5000 4639 9507</div>
<div class="muted">Swift/BIC Code: STSPAT2GXXX</div>
<div class="muted">ATU82089605</div>
</div>
</div>
<div style="display:flex; justify-content: space-between; gap: 12px; align-items: flex-end;">
<div>
<h1>An die</h1>
<p class="muted" style="margin: 4px 0 0;">Bitte alle Felder vollständig ausfüllen und Zutreffendes ankreuzen.</p>
</div>
<div class="metaGrid">
<div>Vertragsnummer:</div>
<div><span class="fill">{{contractNumber}}</span></div>
<div>Datum:</div>
<div><span class="fill">{{currentDate}}</span></div>
</div>
</div>
<div class="box">
<div class="row">
<div class="label">Empfänger</div>
<div class="value"><span class="fill wide">{{recipientName}}</span></div>
</div>
<div class="row">
<div class="label">Adresse</div>
<div class="value"><span class="fill full">{{recipientAddress}}</span></div>
</div>
</div>
<div class="box">
<div class="row">
<div class="label">AFFILIATE NAME</div>
<div class="value"><span class="fill wide"></span></div>
</div>
<div class="row">
<div class="label">CLIENT NAME</div>
<div class="value"><span class="fill wide">{{fullName}}</span></div>
</div>
</div>
<h2>Lieferadresse</h2>
<div class="box">
<div class="grid2">
<div>
<div class="checkline" style="padding-top: 0;">
<span class="check"><span class="checkbox {{shippingCustomerClass}}"></span> KUNDE</span>
<span class="check"><span class="checkbox {{shippingCompanyClass}}"></span> FIRMA</span>
</div>
<div class="row"><div class="label">Vor- und Nachname</div><div class="value"><span class="fill wide">{{shippingFullName}}</span></div></div>
<div class="row"><div class="label">Adresse</div><div class="value"><span class="fill full">{{shippingStreet}}</span></div></div>
<div class="row"><div class="label">PLZ / Ort</div><div class="value"><span class="fill">{{shippingPostalCode}}</span> <span class="fill wide">{{shippingCity}}</span></div></div>
</div>
<div>
<div class="row"><div class="label">Telefonnummer</div><div class="value"><span class="fill">{{shippingPhone}}</span></div></div>
<div class="row"><div class="label">Mobil</div><div class="value"><span class="fill"></span></div></div>
<div class="row"><div class="label">E-Mail-Adresse</div><div class="value"><span class="fill wide">{{shippingEmail}}</span></div></div>
</div>
</div>
<div class="checkline" style="margin-top: 6px;">
<span class="check"><strong>Rechnungsadresse:</strong></span>
<span class="check">{{invoiceSameAsShippingMark}} wie Lieferadresse</span>
</div>
</div>
<h2>Rechnungsadresse</h2>
<div class="box">
<div class="grid2">
<div>
<div class="checkline" style="padding-top: 0;">
<span class="check"><span class="checkbox {{invoiceCustomerClass}}"></span> KUNDE</span>
<span class="check"><span class="checkbox {{invoiceCompanyClass}}"></span> FIRMA</span>
</div>
<div class="row"><div class="label">Vor- und Nachname</div><div class="value"><span class="fill wide">{{invoiceFullName}}</span></div></div>
<div class="row"><div class="label">Adresse</div><div class="value"><span class="fill full">{{invoiceStreet}}</span></div></div>
<div class="row"><div class="label">PLZ / Ort</div><div class="value"><span class="fill">{{invoicePostalCode}}</span> <span class="fill wide">{{invoiceCity}}</span></div></div>
</div>
<div>
<div class="row"><div class="label">Telefonnummer</div><div class="value"><span class="fill">{{invoicePhone}}</span></div></div>
<div class="row"><div class="label">Mobil</div><div class="value"><span class="fill"></span></div></div>
<div class="row"><div class="label">E-Mail-Adresse</div><div class="value"><span class="fill wide">{{invoiceEmail}}</span></div></div>
</div>
</div>
<div class="checkline" style="margin-top: 6px;">
<span class="check"><span class="checkbox {{fnCheckedClass}}"></span> FN <span class="fill">{{fnNumber}}</span></span>
<span class="check"><span class="checkbox {{atuCheckedClass}}"></span> ATU <span class="fill">{{atuNumber}}</span></span>
</div>
</div>
<p><strong>Zutreffendes bitte ankreuzen:</strong></p>
<div class="box">
<div class="checkline" style="align-items:flex-start;">
<span class="check"><span class="checkbox {{entrepreneurClass}}"></span></span>
<span>Der Kunde/Käufer tätigt das gegenständliche Rechtsgeschäft als <strong>Unternehmer</strong> im Sinne des § 1 Abs 1 Z 1 KSchG, das heißt, das Geschäft gehört zum Betrieb seines Unternehmens.</span>
</div>
<div class="checkline" style="align-items:flex-start;">
<span class="check"><span class="checkbox {{consumerClass}}"></span></span>
<span>Der Kunde/Käufer tätigt das gegenständliche Rechtsgeschäft als <strong>Konsument</strong> im Sinne des § 1 Abs 1 Z 2 KSchG.</span>
</div>
</div>
<p><strong>ANGEBOTE</strong></p>
<p><strong>Mindestbestellmenge </strong>für BIO Kaffee und BIO Tee und BIO Kakao beträgt pro Bestellung jeweils <strong>60 Kapseln</strong>. </p>
<p>Preise und Konditionen gemäß gültigem <strong>PROFIT PLANET GMBH</strong> Tarif.</p>
<table>
<tr>
<td></td>
<td>
<p><strong>VITAMIN KAFFEE</strong></p>
</td>
<td>
<p><strong>MATCHA &amp; DUBAI</strong></p>
</td>
<td>
<p><strong>BASIC KAFFEE</strong></p>
</td>
</tr>
<tr>
<td>
<p>Preis ohne ABO ( BRUTTO )</p>
</td>
<td>
<p><strong>2.97,-€</strong></p>
</td>
<td>
<p><strong>1,97,-€</strong></p>
</td>
<td>
<p><strong>0,99,-€</strong></p>
</td>
</tr>
<tr>
<td>
<p>Preis mit ABO ( BRUTTO )</p>
</td>
<td>
<p><strong>1.22,-€</strong></p>
</td>
<td>
<p><strong>0,97,-€</strong></p>
</td>
<td>
<p><strong>0,69,-€</strong></p>
</td>
</tr>
<tr>
<td>
<p>Business ABO ( NETTO )</p>
</td>
<td>
<p><strong>1,00,-€</strong></p>
</td>
<td>
<p><strong>0,79,-€</strong></p>
</td>
<td>
<p><strong>0,56,-€</strong></p>
</td>
</tr>
</table>
<table>
<tr>
<td>
<p><strong>Superfood Coffee</strong></p>
</td>
<td>
<p><strong>____________KAPSELN</strong></p>
</td>
</tr>
<tr>
<td>
<p>Beauty Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Focus Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Focus Superfood Cafe Espresso</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Fitness Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Fitness Superfood Cafe Espresso</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Sleep Well Superfood Cafe Decaf Lungo </p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Glow Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Glow Superfood Cafe Espresso</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Glow Collagen Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Longevity Mushroom Cafe Espresso</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Longevity Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Anti Aging Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Anti Aging Superfood Cafe Espresso</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Beauty Superfood Cafe Espresso Pink Edition</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Beauty Superfood Cafe Espresso Violett Edition</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Coffee Dubai Chocolate Style</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Mighty Matcha Tea</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Coffee Lungo Crema 7</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Coffee Crema 10</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Espresso 12</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Espresso Intenso 13</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Ristretto 14</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Decaffeinato Lungo</p>
</td>
<td></td>
</tr>
</table>
<p>Bei Angabe einer automatischen Wiederbestellung, gemäß den Regelungen in nachstehendem <strong>Punkt 3</strong>, erhält der Kunde in regelmäßigen Abständen, <strong>BEGINNEND AM </strong>(Unterzeichnung des Vertrages) vorstehend eingetragene BIO Kaffee-Teemenge für die Dauer des Vertrages oder bis zum Widerruf der automatischen Wiederbestellung. Der BIO Kaffee-Tee wird automatisch <strong>im Abstand von</strong> (zutreffendes bitte ankreuzen)</p>
<p> <input checked="checked" type="checkbox" /> 1 Monat </p>
<p>fakturiert und innerhalb von drei bis fünf Werktagen an den Kunden geliefert. </p>
<p> </p>
<p><strong>Bitte wählen Sie Ihre Zahlungsart: </strong></p>
<div class="box">
<div class="checkline">
<span class="check"><span class="checkbox {{paymentSepaClass}}"></span> <strong>Sepa</strong></span>
</div>
<div class="checkline">
<span class="check"><span class="checkbox {{invoiceByEmailClass}}"></span> <strong>Bitte senden Sie mir meine Rechnung per E-Mail zu!</strong></span>
</div>
</div>
<p><strong>§ 1 Vertragsgegenstand/Geltung der Allgemeinen Geschäftsbedingungen</strong></p>
<ol>
<li><a id="_Hlk68781926"></a>Die Allgemeinen Geschäftsbedingungen (AGB, siehe unten) der Profit Planet GmbH sind verbindlicher Bestandteil dieses Vertrages. Abweichende, entgegenstehende oder ergänzende Allgemeine Geschäftsbedingungen des Kunden werden nur dann und insoweit Vertragsbestandteil, <a id="_Hlk73957015"></a>als Profit Planet GmbH ihrer Geltung ausdrücklich zugestimmt hat. Dieses Zustimmungserfordernis gilt in jedem Fall, beispielsweise auch dann, wenn Profit Planet GmbH in Kenntnis der AGB des Kunden die Lieferung an ihn vorbehaltlos ausführt. </li>
</ol>
<p><strong>§ 2 Laufzeit des Vertrages/Zahlung per Lastschrift/Abrechnung</strong></p>
<ol>
<li>Der Vertrag hat eine Laufzeit von <strong>36 Monaten. </strong></li>
<li>Ist der Kunde Unternehmer, verlängert sich der Vertrag nach Ablauf der 36 Monate jeweils um 3 Monate, sofern er nicht von einer der Parteien mit einer Frist von 4 Wochen vor Vertragsende gekündigt wird.</li>
<li>Ist der Kunde Verbraucher, wird die Profit Planet GmbH den Verbraucher spätestens drei Monate und frühestens fünf Monate vor Ablauf der Vertragsdauer in Textform auf das bevorstehende Vertragsende und die automatische Verlängerung hinweisen. Erfolgt kein solcher Hinweis, endet der Vertrag mit Ablauf der ursprünglichen Vertragsdauer. Nach rechtzeitigem Hinweis verlängert sich der Vertrag auch mit Verbrauchern um jeweils 3 Monate, wenn er nicht bis spätestens 4 Wochen vor Ablauf der jeweiligen Vertragslaufzeit von einer der Parteien gekündigt wird.</li>
<li>Die Zahlung per Kreditkarte, Lastschrift/Bankeinzug, Rechnung und Nachnahme ist Voraussetzung für den Vertrag (SEPA Lastschrift-Mandat). </li>
</ol>
<p><strong>§ 3 Automatische Wiederbestellungen </strong></p>
<ol>
<li>Bei Angabe einer automatischen Wiederbestellung durch den Kunden wird diesem im gewählten Bestellintervall BIO Kaffee, Tee gemäß der auf Seite 2,3,4 und 5 stehenden Tabelle gewählte Menge an die aktuelle Lieferadresse geschickt. Die Zusammenstellung der Kaffee Teevarietäten kann bei schriftlichem Einlangen des Änderungswunsches bis zwei Werktage von dem Kunden gewählten Versanddatum geändert werden.</li>
</ol>
<p><strong>§ 4 Verpflichtung zur Verwendung von Produkten der Profit Planet GmbH Vertragsstrafe/Liefervereinbarung</strong></p>
<ol>
<li>Der Kunde verpflichtet sich, während der Laufzeit des Vertrags auf den von Profit Planet GmbH zur Verfügung gestellten Kaffeemaschinen ausschließlich Produkte <a id="_Hlk74558723"></a>der <a id="_Hlk73956971"></a>Profit Planet GmbH ,,LANATURA, zu verwenden und einzusetzen, maximal jedoch für einen Zeitraum von drei (3) und/oder fünf (5) Jahren ab Vertragsschluss.</li>
<li>Der Kaffee wird wiederkehrend zugestellt laut Bestellung.</li>
<li>Verstößt der Kunde gegen seine Verpflichtung aus Abs. 1, so ist die Profit Planet GmbH zur außerordentlichen fristlosen Kündigung aus wichtigem Grund berechtigt. Darüber hinaus vereinbaren die Parteien die Zahlung einer verschuldensunabhängigen Vertragsstrafe durch den Kunden an die Profit Planet GmbH in angemessener Höhe, wobei die Profit Planet GmbH die Höhe nach billigem Ermessen bestimmen wird und die Angemessenheit der Vertragsstrafe im Streitfall von dem zuständigen Gericht überprüft werden kann. Die Geltendmachung weiteren Schadensersatzes bleibt vorbehalten. </li>
</ol>
<p><strong>§ 5 Wartung und Reparatur</strong></p>
<p> (1) Wartung und Reparaturen der Kaffeemaschinen sind in der Gestellung wie folgt enthalten:</p>
<ol>
<li>Alle Wartungsarbeiten und Reparaturen werden werktags, von Montag bis Freitag zu den üblichen Arbeitszeiten (09:00 - 17:00 Uhr) Telefonisch: 0043 676 3440274 oder schriftlich an office@profit-planet.com durchgeführt. Dienstleistungen an Wochenenden und Feiertagen sind ausgeschlossen und können nur gegen einen Aufpreis vom Kunden selbst bei dem von uns benannten, autorisierten Servicepartner beauftragt werden.</li>
<li>Nicht eingeschlossen sind Reparaturen, die auf mangelhafte Pflege oder eine unsachgemäße Bedienung zurückzuführen sind. Insbesondere die Nichtbeachtung der Bedienungs- und Reinigungsanleitung, die unsachgemäße oder mangelnde Entkalkung sowie Bedienungsfehler gehen zu Lasten des Kunden.</li>
<li>Profit Planet GmbH behält sich vor, dem Kunden das Ersatzgerät zum UVP zu fakturieren, sofern das Ersatzgerät nicht innerhalb von 4 Wochen nach Erhalt der reparierten Maschine an den Kundendienst zurückgeschickt wurde.</li>
</ol>
<p> </p>
<ol>
<li>Ausdrücklich nicht von dem vertraglichen Wartungs- und Reparaturservice umfasst sind: Stellplatzwechsel, Produktumstellung, Verkostung, Umbauten, die Behebung von Störungen, die als Folge von Reparaturen oder Änderungen durch den Kunden oder durch Dritte auftreten, die Behebung von Störungen, die durch unsachgemäße Bedienung oder mangelhafte Reinigung bzw. Pflege verursacht wurden, die Behebung von Störungen, deren Ursache außerhalb der Kaffeemaschine liegen, wie Defekte in der Wasser- und Stromzufuhr, Elementarschäden, Missbrauch, andere außergewöhnliche Einwirkungen und Fremdkörper. </li>
</ol>
<p><strong>§ 6 Außerordentliche Kündigung</strong></p>
<ol>
<li>Beide Vertragsparteien haben das Recht, diesen Vertrag außerordentlich fristlos zu kündigen, wenn die jeweils andere Vertragspartei gegen wesentliche Bestimmungen dieses Vertrages trotz Mahnung und angemessener Frist verstößt.</li>
<li>
Profit Planet GmbH ist insbesondere berechtigt, den Vertrag außerordentlich fristlos zu kündigen, wenn der Kunde
<ol>
<li>mit einer Zahlung ganz oder teilweise im Verzug ist und Profit Planet GmbH dem Kunden erfolglos eine angemessene Frist zur Zahlung des rückständigen Betrages gesetzt hat, oder</li>
<li>eine in diesem Vertrag vereinbarte Mindestabnahmemenge in einem Zeitraum von 6 Monaten um mehr als durchschnittlich 20% unterschritten wurde, oder</li>
<li>Fremde Produkte auf den gestellten Kaffeemaschinen zubereitet.</li>
</ol>
</li>
</ol>
<p>(3) Im Falle einer außerordentlichen fristlosen Kündigung durch Profit Planet GmbH, behält sich diese vor, dem Kunden eine Deckungsausgleichzahlung für die Restlaufzeit in Höhe von 25% der vereinbarten Mindestabnahmemenge, sowie der vertraglich vereinbarten Mietzinsen in Rechnung zu stellen. Die Geltendmachung weiteren Schadensersatzes bleibt vorbehalten. Dem Kunden bleibt der Nachweis offen, dass kein oder ein wesentlich geringerer Schaden entstanden ist.</p>
<p><strong>§ 7 Eigentumsverhältnisse</strong></p>
<p>Die gelieferten Maschinen bleiben Eigentum von Profit Planet GmbH.</p>
<p><strong>§ 8 Interne Bestellsysteme und Bestellungen, Datenschutz</strong></p>
<p>Weiteres stimme ich dem Erhalt von exklusiven Angeboten und Informationen wie folgt zu:</p>
<p><input type="checkbox" /> Ich stimme zu, dass die angegebenen Daten von der Profit Planet GmbH verarbeitet und zur Information über exklusive Angebote und sonstige Informationen über E-Mail-Newsletters verwendet werden. Die Zustimmung kann jederzeit per E-Mail an office@profit-planet.com widerrufen werden. Ich akzeptiere hiermit die Datenschutzbestimmungen.</p>
<p><input type="checkbox" /> Ich stimme zu, dass die angegebenen Daten von der Profit Planet GmbH verarbeitet und zur telefonischen Information über exklusive Angebote und sonstige Informationen verwendet werden. Die Zustimmung kann jederzeit per E-Mail an office@profit-planet.com widerrufen werden. Ich akzeptiere hiermit die Datenschutzbestimmungen.</p>
<p>Sie haben uns schon früher Ihre Zustimmung gegeben und erhalten schon Informationen und Angebote zu unseren Produkten, wollen diese Einwilligung aber jetzt widerrufen: Diese Zustimmung kann jederzeit per E-Mail an office@profit-planet.com widerrufen werden.</p>
<p><strong>Mit dieser Unterschrift wird (werden) das (die) auf Seite 1 genannte (n) Gerät (e) zu genannten Konditionen übernommen.</strong></p>
<p>Es gelten die <strong>Allgemeinen Geschäftsbedingungen</strong> der Profit Planet GmbH als vereinbart. Der Kunde erklärt hiermit ausdrücklich, dass er die <strong>Allgemeinen Geschäftsbedingungen</strong> und die <strong>Datenschutzbestimmungen</strong> gelesen hat und diesen zustimmt. Der Vertrag kommt mittels Annahme durch Profit Planet GmbH<strong> </strong>zustande und ist unter der Voraussetzung einer positiven Bonitätsprüfung gültig.</p>
<p>Ort: {{signingCity}} Datum: {{currentDate}}</p>
<div class="signature-grid">
<div class="signature-box">
<div class="pp-stamp">{{profitplanetSignature}}</div>
<div>Unterschrift Profit Planet GmbH</div>
</div>
<div class="signature-box">
<div>{{signatureImage}}</div>
<div>Stempel/ Unterschrift Kunde</div>
</div>
</div>
<p><em>Informationen über Inhaltsstoffe, Nährwertangaben etc. finden Sie auf der Lieferanten-Homepage www.lanaturalifestyle.com oder unter der Telefonnummer: 0043 552 322 960 zur Verfügung</em>.</p>
<p><strong>Allgemeine Geschäftsbedingungen Kaffee-Service</strong></p>
<p><strong>§ 1 Geltungsbereich, Form</strong></p>
<ol>
<li>Die vorliegenden Allgemeinen Geschäftsbedingungen (AGB) gelten für alle unsere Geschäftsbeziehungen zwischen unseren Kunden und uns, der Profit Planet GmbH, Liebenauer Hauptstraße 82c, 8041 Graz, Österreich. Die AGB gelten gegenüber Verbrauchern und Unternehmern; zwingende Verbraucherschutzbestimmungen (insbesondere nach KSchG und FAGG) gehen im Zweifel diesen AGB vor. </li>
<li>Die AGB gelten insbesondere für Verträge über den Verkauf und/oder die Lieferung beweglicher Sachen („Ware“), ohne Rücksicht darauf, ob wir die Ware selbst herstellen oder bei Zulieferern einkaufen. Sofern nichts anderes vereinbart, gelten die AGB in der zum Zeitpunkt der Bestellung des Käufers gültigen bzw. jedenfalls in der ihm zuletzt in Textform mitgeteilten Fassung als Rahmenvereinbarung auch für gleichartige künftige Verträge, ohne dass wir in jedem Einzelfall wieder auf sie hinweisen müssten.</li>
<li>Unsere AGB gelten ausschließlich. Abweichende, entgegenstehende oder ergänzende Allgemeine Geschäftsbedingungen des Käufers werden nur dann und insoweit Vertragsbestandteil, als wir ihrer Geltung ausdrücklich zugestimmt haben. Dieses Zustimmungserfordernis gilt in jedem Fall, beispielsweise auch dann, wenn wir in Kenntnis der AGB des Käufers die Lieferung an ihn vorbehaltlos ausführen.</li>
<li>Im Einzelfall getroffene, individuelle Vereinbarungen mit dem Käufer (einschließlich Nebenabreden, Ergänzungen und Änderungen) haben in jedem Fall Vorrang vor diesen AGB. Für den Inhalt derartiger Vereinbarungen ist, vorbehaltlich des Gegenbeweises, ein schriftlicher Vertrag bzw. unsere schriftliche Bestätigung maßgebend.</li>
<li>Rechtserhebliche Erklärungen und Anzeigen des Käufers in Bezug auf den Vertrag (z.B. Fristsetzung, Mängelanzeige, Rücktritt oder Minderung), sind schriftlich, d.h. in Schrift- oder Textform (z.B. Brief, E-Mail, Telefax) abzugeben. Gesetzliche Formvorschriften und weitere Nachweise insbesondere bei Zweifeln über die Legitimation des Erklärenden bleiben unberührt.</li>
<li>Hinweise auf die Geltung gesetzlicher Vorschriften haben nur klarstellende Bedeutung. Auch ohne eine derartige Klarstellung gelten daher die gesetzlichen Vorschriften, soweit sie in diesen AGB nicht unmittelbar abgeändert oder ausdrücklich ausgeschlossen werden.</li>
</ol>
<p><strong>§ 2 Vertragsschluss</strong></p>
<ol>
<li>Unsere Angebote sind freibleibend und unverbindlich. Dies gilt auch, wenn wir dem Käufer Kataloge, technische Dokumentationen (z.B. Zeichnungen, Pläne, Berechnungen, Kalkulationen, Verweisungen auf DIN-Normen), sonstige Produktbeschreibungen oder Unterlagen auch in elektronischer Form überlassen haben, an denen wir uns Eigentums- und Urheberrechte vorbehalten.</li>
<li>Die Bestellung der Ware durch den Käufer gilt als verbindliches Vertragsangebot. Sofern sich aus der Bestellung nichts anderes ergibt, sind wir berechtigt, dieses Vertragsangebot innerhalb von 30 Tagen nach seinem Zugang bei uns anzunehmen.</li>
<li>Die Annahme kann entweder schriftlich (z.B. durch Auftragsbestätigung) oder durch Auslieferung der Ware an den Käufer erklärt werden.</li>
</ol>
<p><strong>§ 3 Lieferfrist und Lieferverzug</strong></p>
<ol>
<li>Die Lieferfrist wird individuell vereinbart bzw. von uns bei Annahme der Bestellung angegeben. Sofern dies nicht der Fall ist, beträgt die Lieferfrist ca. 14 21 Tage ab Vertragsschluss.</li>
<li>Sofern wir verbindliche Lieferfristen aus Gründen, die wir nicht zu vertreten haben, nicht einhalten können (Nichtverfügbarkeit der Leistung), werden wir den Kunden hierüber unverzüglich informieren und gleichzeitig die voraussichtliche, neue Lieferfrist mitteilen. Ist die Leistung auch innerhalb der neuen Lieferfrist nicht verfügbar, sind wir berechtigt, ganz oder teilweise vom Vertrag zurückzutreten; eine bereits erbrachte Gegenleistung des Kunden werden wir unverzüglich erstatten. Als Fall der Nichtverfügbarkeit der Leistung in diesem Sinne gilt insbesondere die nicht rechtzeitige Selbstbelieferung durch unseren Zulieferer, wenn wir ein kongruentes Deckungsgeschäft abgeschlossen haben, weder uns noch unseren Zulieferer ein Verschulden trifft oder wir im Einzelfall zur Beschaffung nicht verpflichtet sind.</li>
<li>Der Eintritt unseres Lieferverzugs bestimmt sich nach den gesetzlichen Vorschriften. In jedem Fall ist aber eine Mahnung durch den Kunden erforderlich.</li>
</ol>
<p> </p>
<ol>
<li>Die Rechte des Kunden und unsere gesetzlichen Rechte, insbesondere bei einem Ausschluss der Leistungspflicht (z.B. aufgrund Unmöglichkeit oder Unzumutbarkeit der Leistung und/oder Nacherfüllung), bleiben unberührt.</li>
</ol>
<p><strong>§ 4 Lieferung, Gefahrübergang, Abnahme, Annahmeverzug</strong></p>
<ol>
<li>Die Lieferung erfolgt ab Lager, wo auch der Erfüllungsort für die Lieferung und eine etwaige Nacherfüllung ist. Auf Verlangen und Kosten des Käufers wird die Ware an einen anderen Bestimmungsort versandt (Versendungskauf). Soweit nicht etwas anderes vereinbart ist, sind wir berechtigt, die Art der Versendung (insbesondere Transportunternehmen, Versandweg, Verpackung) selbst zu bestimmen.</li>
<li>Die Gefahr des zufälligen Untergangs und der zufälligen Verschlechterung der Ware geht spätestens mit der Übergabe auf den Käufer über. Beim Versendungskauf geht die Gefahr des zufälligen Untergangs und einer zufälligen Verschlechterung der Ware während des Transports für Unternehmer bereits mit Übergabe der Ware an den Spediteur/Frachtführer über; für Verbraucher geht die Gefahr erst über, wenn die Ware dem Verbraucher oder einem von diesem benannten Dritten (der nicht Frachtführer ist) übergeben wurde. Hat der Verbraucher den Beförderungsvertrag selbst ohne unsere Auswahlmöglichkeit beauftragt, so geht die Gefahr bereits mit Übergabe der Ware an den Beförderer über.</li>
<li>Kommt der Käufer in Annahmeverzug, unterlässt er eine Mitwirkungshandlung oder verzögert sich unsere Lieferung aus anderen, vom Käufer zu vertretenden Gründen, so sind wir berechtigt, Ersatz des hieraus entstehenden Schadens einschließlich Mehraufwendungen (z.B. Lagerkosten) zu verlangen. Der Nachweis eines höheren Schadens und unsere gesetzlichen Ansprüche (insbesondere Ersatz von Mehraufwendungen, angemessene Entschädigung, Kündigung) bleiben unberührt. </li>
</ol>
<p><strong>§ 5 Preise und Zahlungsbedingungen</strong></p>
<ol>
<li>Sofern im Einzelfall nichts anderes vereinbart ist, gelten unsere jeweils zum Zeitpunkt des Vertragsschlusses aktuellen Preise, und zwar ab Lager, zzgl. gesetzlicher Umsatzsteuer.</li>
<li>Beim Versendungskauf trägt der Käufer die Transportkosten ab Lager und die Kosten einer ggf. vom Käufer gewünschten Transportversicherung. Sofern wir nicht die im Einzelfall tatsächlich entstandenen Transportkosten in Rechnung stellen, gilt eine Transportkostenpauschale (ausschließlich Transportversicherung) iHv 200 EUR als vereinbart. Etwaige Zölle, Gebühren, Steuern und sonstige öffentliche Abgaben trägt der Käufer.</li>
<li>Der Kaufpreis ist fällig und zu zahlen innerhalb von 14 Tagen ab Rechnungsstellung und Lieferung bzw. Abnahme der Ware. Wir sind jedoch, auch im Rahmen einer laufenden Geschäftsbeziehung, jederzeit berechtigt, eine Lieferung ganz oder teilweise nur gegen Vorkasse durchzuführen. Einen entsprechenden Vorbehalt erklären wir spätestens mit der Auftragsbestätigung.</li>
<li>Mit Ablauf vorstehender Zahlungsfrist kommt der Käufer in Verzug. Der Kaufpreis ist während des Verzugs zum jeweils geltenden gesetzlichen Verzugszinssatz zu verzinsen. Wir behalten uns die Geltendmachung eines weitergehenden Verzugsschadens vor. Gegenüber Kaufleuten bleibt unser Anspruch auf den kaufmännischen Fälligkeitszins (§ 352 UGB) unberührt.</li>
<li>Dem Käufer stehen Aufrechnungs- oder Zurückbehaltungsrechte nur insoweit zu, als sein Anspruch rechtskräftig festgestellt oder unbestritten ist. Bei Mängeln der Lieferung bleiben die Gegenrechte des Käufers insbesondere gem. § 7 dieser AGB unberührt. Gegenüber Verbrauchern gilt diese Einschränkung nicht für Ansprüche, die in rechtlichem Zusammenhang mit ihrer Verbindlichkeit stehen.</li>
<li>Wird nach Vertragsabschluss erkennbar, dass unser Anspruch auf Zahlung des Kaufpreises durch mangelnde Zahlungsfähigkeit oder drohende Zahlungsunfähigkeit des Käufers gefährdet ist etwa durch Antrag auf Eröffnung eines Insolvenzverfahrens oder vergleichbare Umstände , sind wir berechtigt, unsere Leistung zu verweigern und dem Käufer eine angemessene Frist zur Erbringung der Gegenleistung oder zur Sicherheitsleistung zu setzen. Nach fruchtlosem Ablauf dieser Frist sind wir berechtigt, vom Vertrag zurückzutreten.</li>
</ol>
<p><strong>§ 6 Eigentumsvorbehalt</strong></p>
<ol>
<li>Bis zur vollständigen Bezahlung aller unserer gegenwärtigen und künftigen Forderungen aus dem Kaufvertrag und einer laufenden Geschäftsbeziehung (gesicherte Forderungen) behalten wir uns das Eigentum an den verkauften Waren vor.</li>
<li>Die unter Eigentumsvorbehalt stehenden Waren dürfen vor vollständiger Bezahlung der gesicherten Forderungen weder an Dritte verpfändet noch zur Sicherheit übereignet werden. Der Käufer hat uns unverzüglich schriftlich zu benachrichtigen, wenn ein Antrag auf Eröffnung eines Insolvenzverfahrens gestellt oder so weit Zugriffe Dritter (bzw. Pfändungen) auf die uns gehörenden Waren erfolgen.</li>
<li>Bei vertragswidrigem Verhalten des Käufers, insbesondere bei Nichtzahlung des fälligen Kaufpreises, sind wir berechtigt, nach den gesetzlichen Vorschriften vom Vertrag zurückzutreten oder/und die Ware auf Grund des Eigentumsvorbehalts herauszuverlangen. Das Herausgabeverlangen beinhaltet nicht zugleich die Erklärung des Rücktritts; wir sind vielmehr berechtigt, lediglich die Ware herauszuverlangen und uns den Rücktritt vorzubehalten. Zahlt der Käufer den fälligen Kaufpreis nicht, dürfen wir diese Rechte nur geltend machen, wenn wir dem Käufer zuvor erfolglos eine angemessene Frist zur Zahlung gesetzt haben oder eine derartige Fristsetzung nach den gesetzlichen Vorschriften entbehrlich ist.</li>
</ol>
<p>.</p>
<p><strong>§ 7 Sachmängel</strong></p>
<p> (1) In dringenden Fällen, z.B. bei Gefährdung der Betriebssicherheit oder zur Abwehr unverhältnismäßiger Schäden, hat der Käufer das Recht, den Mangel selbst zu beseitigen und von uns Ersatz der hierzu objektiv erforderlichen (angemessenen) Aufwendungen zu verlangen. Von einer derartigen Selbstvornahme sind wir unverzüglich nach Möglichkeit vorher zu benachrichtigen. Das Selbstvornahmerecht besteht nicht, wenn wir berechtigt wären, eine entsprechende Nacherfüllung nach den gesetzlichen Vorschriften zu verweigern.</p>
<p> (2) Wenn die Nacherfüllung fehlgeschlagen ist oder eine für die Nacherfüllung vom Käufer zu setzende angemessene Frist erfolglos abgelaufen oder nach den gesetzlichen Vorschriften entbehrlich ist, kann der Käufer vom Kaufvertrag zurücktreten oder den Kaufpreis mindern. Bei einem unerheblichen Mangel besteht jedoch kein Rücktrittsrecht. Bei Verbrauchern gelten unbeschadet vorstehender Regelungen die gesetzlichen Gewährleistungsrechte uneingeschränkt. Insbesondere beträgt die Gewährleistungsfrist für Verbraucher zwei Jahre ab Übergabe der Ware.</p>
<p> (3) Ansprüche des Käufers auf Schadensersatz bzw. Ersatz vergeblicher Aufwendungen bestehen auch bei Mängeln nur nach Maßgabe von § 8 und sind im Übrigen ausgeschlossen.</p>
<p><strong>§ 8 Sonstige Haftung</strong></p>
<p>(1) Soweit sich aus diesen AGB einschließlich der nachfolgenden Bestimmungen nichts anderes ergibt, haften wir bei der Verletzung vertraglicher und außervertraglicher Pflichten nach den gesetzlichen Vorschriften.</p>
<p>(2) Wir haften gleich aus welchem Rechtsgrund im Rahmen der Verschuldenshaftung bei Vorsatz und grober Fahrlässigkeit. Bei leichter Fahrlässigkeit haften wir nur</p>
<p>a) für Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit,</p>
<p>b) für Schäden aus der Verletzung wesentlicher Vertragspflichten (d.h. solcher Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Vertragspartner regelmäßig vertrauen darf). In diesem Fall ist unsere Haftung jedoch auf den Ersatz des typischen, vorhersehbaren Schadens begrenzt.</p>
<p>(3) Die vorstehenden Haftungsbeschränkungen gelten auch zugunsten Dritter sowie für Pflichtverletzungen durch Personen, deren Verschulden uns nach den gesetzlichen Vorschriften zuzurechnen ist. Sie gelten nicht, soweit wir einen Mangel arglistig verschwiegen oder eine Garantie für die Beschaffenheit der Ware übernommen haben sowie bei Ansprüchen nach dem Produkthaftungsgesetz.</p>
<p>(4) Soweit gesetzlich zulässig, haften wir nicht für mittelbare Schäden, Folgeschäden oder entgangenen Gewinn. Gegenüber Verbrauchern gilt dieser Haftungsausschluss nicht, soweit ein kausaler Zusammenhang mit der Verletzung wesentlicher Vertragspflichten besteht.</p>
<p>(5) Ein Rücktritt oder eine Kündigung wegen Pflichtverletzung, die nicht auf einem Mangel der Ware beruht, ist nur zulässig, wenn wir diese zu vertreten haben. Ein darüberhinausgehendes freies Rücktritts- oder Kündigungsrecht des Käufers wird soweit rechtlich zulässig ausgeschlossen.</p>
<p>(6) Die zwingenden Bestimmungen des Produkthaftungsgesetzes sowie die Haftung für vorsätzliches oder grob fahrlässiges Verhalten bleiben von den vorstehenden Regelungen unberührt.</p>
<p><strong>§ 9 Verjährung</strong></p>
<p>(1) Bei Verträgen mit Unternehmern im Sinne des §1 KSchG wird die gesetzliche Gewährleistungsfrist für bewegliche Sachen gemäß §933 ABGB auf ein Jahr ab Übergabe verkürzt. Dies gilt nicht bei Arglist oder bei Übernahme einer Garantie für die Beschaffenheit der Ware.</p>
<p>(2) Die in Abs. 1 genannte Fristverkürzung gilt nicht für Ansprüche des Käufers wegen Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit, bei grob fahrlässigem oder vorsätzlichem Verhalten oder bei Ansprüchen nach dem Produkthaftungsgesetz.</p>
<p>(3) Schadenersatzansprüche wegen Mängeln (§933a ABGB) verjähren unabhängig von einer verkürzten Gewährleistungsfrist innerhalb der gesetzlichen Frist von drei Jahren ab Kenntnis von Schaden und Schädiger.</p>
<p>(4) Gegenüber Verbrauchern gelten uneingeschränkt die gesetzlichen Gewährleistungs- und Verjährungsfristen (§§922 ff ABGB, §9 KSchG).</p>
<p><strong>§ 10 Rechtswahl und Gerichtsstand</strong></p>
<p>(1) Für sämtliche Rechtsverhältnisse zwischen uns und dem Käufer gilt ausschließlich des materiellen Rechtes der Republik Österreich unter Ausschluss des UN-Kaufrechts (CISG) und sonstiger internationaler Kollisionsnormen, soweit zwingende Verbraucherschutzvorschriften nicht entgegenstehen.</p>
<p>(2) Ist der Käufer Unternehmer im Sinne des §1 KSchG, so wird für alle Streitigkeiten aus oder im Zusammenhang mit diesem Vertrag einschließlich seiner Gültigkeit und Durchführung das sachlich zuständige Gericht in Graz vereinbart. Wir sind jedoch berechtigt, auch am allgemeinen Gerichtsstand des Käufers oder an einem sonst gesetzlich zulässigen Gerichtsstand Klage zu erheben.</p>
<p>(3) Gegenüber Verbrauchern gelten die gesetzlichen Gerichtsstandregelungen. Eine abweichende Gerichtsstandsvereinbarung mit Verbrauchern wird nicht getroffen.</p>
<p><strong>§ 11 Schlussbestimmungen</strong></p>
<p>Sollten einzelne Bestimmungen dieses Vertrages unwirksam oder nichtig sein oder werden, so berührt dies die Gültigkeit der übrigen Bestimmungen dieses Vertrages nicht. Die Parteien verpflichten sich, unwirksame oder nichtige Bestimmungen durch neue Bestimmungen zu ersetzen, die dem in den unwirksamen oder nichtigen Bestimmungen enthaltenen wirtschaftlichen Regelungsgehalt in rechtlich zulässiger Weise gerecht werden. Entsprechendes gilt, wenn sich in dem Vertrag eine Lücke herausstellen sollte. Zur Ausfüllung der Lücke verpflichten sich die Parteien auf die Etablierung angemessener Regelungen in diesem Vertrag hinzuwirken, die dem am nächsten kommen, was die Vertragsschließenden nach dem Sinn und Zweck dieses Vertrages bestimmt hätten, wenn der Punkt von ihnen bedacht worden wäre.</p>
<p>Stand der Allgemeinen Geschäftsbedingungen Profit Planet Kaffee-Service: 01.08.2025</p>
<table>
<tr>
<td></td>
</tr>
</table>
<p><a id="page4"></a></p>
<p><strong>Informationen für Verbraucher über das Rücktrittsrecht (Widerrufsrecht)</strong><br /></p>
<p><em>Widerrufsrecht:</em> Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen. Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem Sie (oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist) die erste Ware im Rahmen dieses Vertrages in Besitz genommen haben. Bei einem Vertrag über Dienstleistungen (z.B. Miete einer Kaffeemaschine) beginnt die Widerrufsfrist mit dem Tag des Vertragsabschlusses.</p>
<p>Um Ihr Widerrufsrecht auszuüben, müssen Sie uns (Profit Planet GmbH, Liebenauer Hauptstraße 82c, 8041 Graz, E-Mail: office@profit-planet.com) mittels einer eindeutigen Erklärung (z.B. ein mit der Post versandter Brief oder E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das unten angefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist. Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.</p>
<p><em>Folgen des Widerrufs:</em> Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben einschließlich etwaiger Lieferkosten (mit Ausnahme jener zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung, als die von uns angebotene günstigste Standardlieferung gewählt haben)  unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart. Ihnen werden wegen dieser Rückzahlung keine Entgelte berechnet.</p>
<p>Handelt es sich bei dem widerrufenen Vertrag um einen Kaufvertrag über Waren, können wir die Rückzahlung verweigern, bis wir die Waren wieder zurückerhalten haben oder Sie den Nachweis erbracht haben, dass Sie die Waren abgesandt haben  je nachdem, welcher Zeitpunkt früher eintritt. Sie haben die Waren in diesem Fall unverzüglich und in jedem Fall spätestens binnen vierzehn Tagen ab dem Tag, an dem Sie uns über den Widerruf dieses Vertrags unterrichten, an uns zurückzusenden oder zu übergeben. Die Frist ist gewahrt, wenn Sie die Waren vor Ablauf der Frist von vierzehn Tagen absenden. Sie tragen die unmittelbaren Kosten der Rücksendung der Waren.</p>
<p>Sie müssen für einen etwaigen Wertverlust der Waren nur aufkommen, wenn dieser Wertverlust auf einen zur Prüfung der Beschaffenheit, Eigenschaften und Funktionsweise der Waren nicht notwendigen Umgang mit ihnen zurückzuführen ist.</p>
<p>Haben Sie verlangt, dass eine Dienstleistung (oder die regelmäßige Lieferung von Waren) während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil, der bis zu dem Zeitpunkt der Widerrufsausübung bereits erbrachten Leistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Leistungen entspricht.</p>
<p><strong>Muster-Widerrufsformular</strong><br /></p>
<p>(Wenn Sie den Vertrag widerrufen wollen, können Sie dieses Formular ausfüllen und an uns zurücksenden.)</p>
<p> An Profit Planet GmbH, Liebenauer Hauptstraße 82c, 8041 Graz, E-Mail: <a href="mailto:office@profit-planet.com">office@profit-planet.com</a>:</p>
<p> Hiermit widerrufe(n) ich/wir (<strong>)</strong></p>
<p><strong>den von mir/uns (</strong>) abgeschlossenen Vertrag über den Kauf der folgenden Ware(n)/die Erbringung der folgenden Dienstleistung</p>
<p> Bestellt am (<strong>) / erhalten am (</strong>)</p>
<p> Name des/der Verbraucher(s)</p>
<p> Anschrift des/der Verbraucher(s)</p>
<p> Datum</p>
<p> Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)</p>
</div>
</body>
</html>

Binary file not shown.

View File

@ -0,0 +1,737 @@
<!DOCTYPE html>
<html lang="sl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>abo-contract-SL</title>
<style>
@page { size: A4; margin: 14mm; }
:root {
--ink: #111827;
--muted: #6b7280;
--line: #d1d5db;
--soft: #f3f4f6;
--soft2: #f9fafb;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
color: var(--ink);
font-family: Arial, Helvetica, sans-serif;
font-size: 11.5pt;
line-height: 1.35;
background: #ffffff;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
body { padding: 0; }
p, li, td, th {
white-space: pre-wrap;
}
.document {
width: 100%;
max-width: 860px;
margin: 0 auto;
padding: 0 16px 18px;
}
.header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
padding: 18px 0 10px;
border-bottom: 2px solid var(--ink);
margin-bottom: 12px;
}
.brand .title {
font-weight: 800;
letter-spacing: 0.2px;
font-size: 16pt;
margin: 0 0 4px;
}
.brand .subtitle {
margin: 0;
color: var(--muted);
font-size: 10pt;
}
.company {
text-align: right;
font-size: 9.5pt;
max-width: 340px;
}
.metaGrid {
display: grid;
grid-template-columns: auto auto;
justify-content: end;
align-items: end;
column-gap: 8px;
row-gap: 4px;
text-align: right;
font-size: 10pt;
color: var(--muted);
}
h1 {
margin: 10px 0 0;
font-size: 15pt;
letter-spacing: 0.2px;
}
h2 {
margin: 18px 0 8px;
font-size: 12.5pt;
border-bottom: 1px solid var(--line);
padding-bottom: 4px;
}
.box {
border: 1px solid var(--line);
border-radius: 8px;
padding: 10px;
background: var(--soft2);
margin: 10px 0;
}
.grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.row {
display: grid;
grid-template-columns: 180px 1fr;
gap: 8px;
padding: 4px 0;
border-bottom: 1px dashed #e5e7eb;
}
.row:last-child { border-bottom: 0; }
.label {
color: var(--muted);
font-size: 10pt;
}
.value {
font-weight: 600;
}
.fill {
display: inline-block;
min-width: 160px;
border-bottom: 1px solid #9ca3af;
padding: 0 4px 1px;
font-weight: 600;
}
.fill.wide { min-width: 280px; }
.fill.full { display: inline-block; min-width: 100%; }
.checkline {
display: flex;
gap: 18px;
flex-wrap: wrap;
align-items: center;
padding: 6px 0;
}
.check {
display: inline-flex;
gap: 8px;
align-items: center;
font-size: 10.5pt;
}
.checkbox {
width: 14px;
height: 14px;
border: 1px solid var(--ink);
display: inline-block;
border-radius: 2px;
position: relative;
flex: 0 0 auto;
}
.checkbox.checked::after {
content: "";
position: absolute;
left: 3px;
top: 0;
width: 6px;
height: 10px;
border-right: 2px solid var(--ink);
border-bottom: 2px solid var(--ink);
transform: rotate(40deg);
}
table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--line);
margin: 14px 0;
background: #fff;
}
th, td {
border: 1px solid var(--line);
padding: 8px 10px;
vertical-align: top;
text-align: left;
}
th {
background: var(--soft);
}
.signature-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin: 14px 0 10px;
}
.signature-box {
border-top: 1px solid #111827;
padding-top: 8px;
min-height: 130px;
text-align: center;
color: var(--muted);
font-size: 10pt;
}
.pp-stamp,
.pp-stamp img,
.signature-box img {
max-width: 280px;
max-height: 120px;
width: auto;
height: auto;
display: block;
margin: 0 auto 8px;
}
.muted { color: var(--muted); }
@media print {
.header,
.box,
.grid2,
table,
tr,
.signature-grid,
.signature-box {
break-inside: avoid;
page-break-inside: avoid;
}
}
</style>
</head>
<body>
<div class="document">
<div class="header">
<div class="brand">
<p class="title">ABO pogodba</p>
<p class="subtitle">Ponudba za sklenitev kupoprodajne/najemne pogodbe za kapsulo za kavno postrežbo</p>
</div>
<div class="company">
<div><strong>PROFIT PLANET GMBH</strong></div>
<div>Kärntner Straße 227</div>
<div>A-8053 Graz</div>
<div class="muted">FN-649474 i</div>
<div class="muted" style="margin-top:6px;">IBAN: AT16 2081 5000 4639 9507</div>
<div class="muted">SWIFT/BIC koda: STSPAT2GXXX</div>
<div class="muted">ATU82089605</div>
</div>
</div>
<div style="display:flex; justify-content: space-between; gap: 12px; align-items: flex-end;">
<div>
<h1>do</h1>
<p class="muted" style="margin: 4px 0 0;">Prosimo, izpolnite vsa polja in označite ustrezne možnosti.</p>
</div>
<div class="metaGrid">
<div>Številka pogodbe:</div>
<div><span class="fill">{{contractNumber}}</span></div>
<div>Datum:</div>
<div><span class="fill">{{currentDate}}</span></div>
</div>
</div>
<div class="box">
<div class="row">
<div class="label">Prejemnik</div>
<div class="value"><span class="fill wide">{{recipientName}}</span></div>
</div>
<div class="row">
<div class="label">Naslov</div>
<div class="value"><span class="fill full">{{recipientAddress}}</span></div>
</div>
</div>
<div class="box">
<div class="row">
<div class="label">IME PODRUŽNICE</div>
<div class="value"><span class="fill wide"></span></div>
</div>
<div class="row">
<div class="label">IME STRANKE</div>
<div class="value"><span class="fill wide">{{fullName}}</span></div>
</div>
</div>
<h2>Naslov za dostavo</h2>
<div class="box">
<div class="grid2">
<div>
<div class="checkline" style="padding-top: 0;">
<span class="check"><span class="checkbox {{shippingCustomerClass}}"></span> STRANKA</span>
<span class="check"><span class="checkbox {{shippingCompanyClass}}"></span> PODJETJE</span>
</div>
<div class="row"><div class="label">Ime in priimek</div><div class="value"><span class="fill wide">{{shippingFullName}}</span></div></div>
<div class="row"><div class="label">Naslov</div><div class="value"><span class="fill full">{{shippingStreet}}</span></div></div>
<div class="row"><div class="label">Poštna številka / Lokacija</div><div class="value"><span class="fill">{{shippingPostalCode}}</span> <span class="fill wide">{{shippingCity}}</span></div></div>
</div>
<div>
<div class="row"><div class="label">Telefonska številka</div><div class="value"><span class="fill">{{shippingPhone}}</span></div></div>
<div class="row"><div class="label">Mobilni telefon</div><div class="value"><span class="fill"></span></div></div>
<div class="row"><div class="label">E-poštni naslov</div><div class="value"><span class="fill wide">{{shippingEmail}}</span></div></div>
</div>
</div>
<div class="checkline" style="margin-top: 6px;">
<span class="check"><strong>Naslov za izstavitev računa:</strong></span>
<span class="check">{{invoiceSameAsShippingMark}} kot na primer naslov za dostavo</span>
</div>
</div>
<h2>Naslov za izstavitev računa</h2>
<div class="box">
<div class="grid2">
<div>
<div class="checkline" style="padding-top: 0;">
<span class="check"><span class="checkbox {{invoiceCompanyClass}}"></span> PODJETJE</span>
<span class="check"><span class="checkbox {{invoiceCustomerClass}}"></span> STRANKA</span>
</div>
<div class="row"><div class="label">Ime in priimek</div><div class="value"><span class="fill wide">{{invoiceFullName}}</span></div></div>
<div class="row"><div class="label">Naslov</div><div class="value"><span class="fill full">{{invoiceStreet}}</span></div></div>
<div class="row"><div class="label">Poštna številka / Lokacija</div><div class="value"><span class="fill">{{invoicePostalCode}}</span> <span class="fill wide">{{invoiceCity}}</span></div></div>
</div>
<div>
<div class="row"><div class="label">Telefonska številka</div><div class="value"><span class="fill">{{invoicePhone}}</span></div></div>
<div class="row"><div class="label">Mobilni telefon</div><div class="value"><span class="fill"></span></div></div>
<div class="row"><div class="label">E-poštni naslov</div><div class="value"><span class="fill wide">{{invoiceEmail}}</span></div></div>
</div>
</div>
<div class="checkline" style="margin-top: 6px;">
<span class="check"><span class="checkbox {{fnCheckedClass}}"></span> FN <span class="fill">{{fnNumber}}</span></span>
<span class="check"><span class="checkbox {{atuCheckedClass}}"></span> ATU <span class="fill">{{atuNumber}}</span></span>
</div>
</div>
<p><strong>Prosimo, označite ustrezno polje:</strong></p>
<div class="box">
<div class="checkline" style="align-items:flex-start;">
<span class="check"><span class="checkbox {{entrepreneurClass}}"></span></span>
<span>Stranka/kupec sklepa zadevni pravni posel kot podjetnik v smislu 1. odstavka 1. člena Zakona o varstvu potrošnikov (KSchG), kar pomeni, da je posel del poslovanja njegovega podjetja.</span>
</div>
<div class="checkline" style="align-items:flex-start;">
<span class="check"><span class="checkbox {{consumerClass}}"></span></span>
<span>Kupec/stranka sklepa ta pravni posel kot potrošnik v smislu 2. točke 1. odstavka 1. člena Zakona o varstvu potrošnikov (KSchG).</span>
</div>
</div>
<p><strong>PONUDBE</strong></p>
<p><strong>Minimalna količina naročila </strong>Za ekološko kavo, ekološki čaj in ekološki kakav je količina naročila 60 kapsul.</p>
<p>Cene in pogoji so v skladu z veljavno tarifo PROFIT PLANET GMBH.</p>
<table>
<tr>
<td></td>
<td>
<p><strong>Vitamin Kapsul</strong></p>
</td>
<td>
<p><strong>Matcha &amp; Dubai Kapsul</strong></p>
</td>
<td>
<p><strong>Basic Kapsul</strong></p>
</td>
</tr>
<tr>
<td>
<p>Stranka brez naročnine bruto</p>
</td>
<td>
<p><strong>2,97 €</strong></p>
</td>
<td>
<p><strong>1,97 €</strong></p>
</td>
<td>
<p><strong>0,99 €</strong></p>
</td>
</tr>
<tr>
<td>
<p>Stranka z naročnino bruto</p>
</td>
<td>
<p><strong>1,22 €</strong></p>
</td>
<td>
<p><strong>0,97 €</strong></p>
</td>
<td>
<p><strong>0,69 €</strong></p>
</td>
</tr>
<tr>
<td>
<p>Poslovno brez davka neto</p>
</td>
<td>
<p><strong>1,00 €</strong></p>
</td>
<td>
<p><strong>0,79 €</strong></p>
</td>
<td>
<p><strong>0,56 €</strong></p>
</td>
</tr>
</table>
<table>
<tr>
<td>
<p><strong>Superživilska kava</strong></p>
</td>
<td>
<p><strong>___________KAPSUL</strong></p>
</td>
</tr>
<tr>
<td>
<p>Beauty Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Focus Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Focus Superfood Cafe Espresso</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Fitness Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Fitnes Superfood Cafe Espresso</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Sleep Well Superfood Cafe Brez kofeina Lungo </p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Glow Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Kavarna Glow Superfood Espresso</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Glow Collagen Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Kavarna z gobami dolgoživosti Espresso</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Longevity Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Anti Aging Superfood Cafe Lungo Forte Crema</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Espresso s superhrano proti staranju</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Lepotilni Superfood Cafe Espresso Pink Edition</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Beauty Superfood Cafe Espresso Violet Edition</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Kava v Dubaju v čokoladnem slogu</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Močan čaj Matcha</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Kava Lungo Crema 7</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Kava Crema 10</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Espresso 12</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Espresso Intenso 13</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Ristretto 14</p>
</td>
<td></td>
</tr>
<tr>
<td>
<p>Brez kofeina Lungo</p>
</td>
<td></td>
</tr>
</table>
<p>Z določitvijo samodejnega ponovnega naročila v skladu z določbami v točki 3 spodaj bo stranka prej omenjeno količino ekološke kave in čaja prejemala v rednih intervalih, začenši z (datum podpisa pogodbe), za čas trajanja pogodbe ali do preklica samodejnega ponovnega naročila. Ekološka kava in čaj bosta dostavljena samodejno v intervalih (prosimo, označite ustrezno).</p>
<p> <input checked="checked" type="checkbox" />1 mesec </p>
<p>fakturirano in dostavljeno stranki v treh do petih delovnih dneh.</p>
<p> </p>
<p><strong>Izberite način plačila:</strong></p>
<div class="box">
<div class="checkline">
<span class="check"><span class="checkbox {{paymentSepaClass}}"></span> <strong>Sepa</strong></span>
</div>
<div class="checkline">
<span class="check"><span class="checkbox {{invoiceByEmailClass}}"></span> <strong>Prosim, pošljite mi račun po e-pošti!</strong></span>
</div>
</div>
<p><strong>§ 1 Predmet pogodbe/Veljavnost splošnih pogojev poslovanja</strong></p>
<ol>
<li><a id="_Hlk68781926"></a>Splošni pogoji poslovanja (GTC, glej spodaj) družbe Profit Planet GmbH so sestavni in zavezujoči del te pogodbe. Kakršni koli drugačni, nasprotujoči si ali dopolnilni pogoji poslovanja stranke postanejo del pogodbe le, če in v obsegu, v katerem se je družba Profit Planet GmbH izrecno strinjala z njihovo veljavnostjo. Ta zahteva po izrecnem soglasju velja v vseh primerih, vključno z na primer takrat, ko družba Profit Planet GmbH, poznajoč pogoje poslovanja stranke, brez pridržkov dostavi blago stranki.<a id="_Hlk73957015"></a></li>
</ol>
<p><strong>§ 2 Trajanje pogodbe/Plačilo z direktno obremenitvijo/Izstavitev računa</strong></p>
<ol>
<li>ThePogodba ima rok trajanja 36 mesecev.</li>
<li>Če je stranka podjetnik, se pogodba po preteku začetnih 36 mesecev podaljšuje za 3 mesece, razen če jo katera koli stranka ne odpove s štiritedenskim odpovednim rokom pred iztekom pogodbe.</li>
<li>Če je stranka potrošnik, bo družba Profit Planet GmbH potrošnika pisno obvestila o skorajšnji prekinitvi pogodbe in samodejnem podaljšanju najpozneje tri mesece in ne prej kot pet mesecev pred iztekom pogodbe. Če tako obvestilo ni podano, pogodba preneha veljati ob koncu prvotnega pogodbenega obdobja. Po pravočasnem obvestilu se pogodba samodejno podaljšuje za trimesečna obdobja, tudi s potrošniki, razen če jo katera koli stranka ne odpove vsaj štiri tedne pred iztekom ustreznega pogodbenega obdobja.</li>
<li>Plačilo s kreditno kartico, direktno obremenitvijo/bančnim nakazilom, računom in plačilom po povzetju je predpogoj za sklenitev pogodbe (mandat za direktno obremenitev SEPA).</li>
</ol>
<p><strong>§ 3 Samodejna ponovna naročila</strong></p>
<ol>
<li>Če stranka nastavi samodejno ponovno naročanje, bosta organska kava in čaj poslana na njen trenutni naslov za dostavo v izbranem intervalu naročila, glede na količino, prikazano v tabeli na straneh 2, 3, 4 in 5. Izbor vrst kave in čaja se lahko spremeni v dveh delovnih dneh od datuma dostave, ki ga stranka izbere, pod pogojem, da je zahteva za spremembo prejeta v pisni obliki.</li>
</ol>
<p><strong>§ 4 Obveznost uporabe izdelkov podjetja Profit Planet GmbH Kazen/Dobavna pogodba</strong></p>
<ol>
<li>Stranka se zavezuje, da bo med trajanjem pogodbe pozorno upoštevala informacije, ki ji jih posreduje [ponudnik]. Kavni avtomati, ki jih dobavlja Profit Planet GmbH, se lahko uporabljajo izključno za izdelke podjetja Profit Planet GmbH »LANATURA«, vendar največ tri (3) oziroma pet (5) let od sklenitve pogodbe.<a id="_Hlk74558723"></a><a id="_Hlk73956971"></a></li>
<li>Kava bo dostavljena redno glede na naročilo.</li>
<li>Če stranka krši svojo obveznost iz odstavka 1, potem Družba Profit Planet GmbH ima pravico odpovedati pogodbo brez odpovednega roka iz utemeljenega razloga. Poleg tega se stranki strinjata, da mora stranka družbi Profit Planet GmbH plačati razumno kazen, ne glede na krivdo. Družba Profit Planet GmbH bo višino kazni določila po lastni presoji, njeno razumnost pa lahko v primeru spora preveri pristojno sodišče. Pravica do zahtevka za nadaljnjo odškodnino ostaja pridržana.</li>
</ol>
<p><strong>§ 5 Vzdrževanje in popravila</strong></p>
<p> (1) Vzdrževanje in popravila kavnih avtomatov so vključena v določbo, kot sledi:</p>
<ol>
<li>Vsa vzdrževalna in popravila se izvajajo med tednom, od ponedeljka do petka, v rednem delovnem času (9:00 - 17:00). Lahko nas kontaktirate po telefonu na številki +43 676 3440274 ali pisno na <a href="mailto:office@profit-planet.com">office@profit-planet.com</a>. Storitve se izvajajo ob vikendih in praznikih, vendar so te izključene in jih lahko stranka uredi le sama proti doplačilu prek našega pooblaščenega servisnega partnerja.</li>
<li>Popravila, ki so posledica neustreznega vzdrževanja ali nepravilne uporabe, niso vključena. Zlasti neupoštevanje navodil za uporabo in čiščenje, nepravilno ali nezadostno odstranjevanje vodnega kamna ter napake pri uporabi so odgovornost stranke.</li>
<li>Profit Planet GmbHsi pridržuje pravico, da stranki izstavi račun za nadomestno napravo po proizvajalčevi priporočeni maloprodajni ceni (MSRP), če nadomestne naprave ne vrne službi za stranke v 4 tednih od prejema popravljene naprave.</li>
</ol>
<p> </p>
<ol>
<li>V pogodbeno vzdrževanje in popravilo izrecno niso vključene: spremembe lokacije, spremembe izdelkov, degustacije, modifikacije, odpravljanje napak, ki so posledica popravil ali sprememb s strani stranke ali tretjih oseb, odpravljanje napak, ki so posledica nepravilnega upravljanja ali neustreznega čiščenja ali vzdrževanja, odpravljanje napak, katerih vzrok je zunaj kavnega avtomata, kot so napake v oskrbi z vodo in električno energijo, naravne nesreče, zloraba, drugi izredni vplivi in tujki.</li>
</ol>
<p><strong>§ 6Izredna odpoved</strong></p>
<ol>
<li>Obe pogodbeni stranki imata pravico odpovedati to pogodbo brez odpovednega roka, če druga pogodbena stranka kljub opozorilu in razumnemu roku krši bistvene določbe te pogodbe.</li>
<li>
Profit Planet GmbHima še posebej pravico odpovedati pogodbo brez odpovednega roka, če stranka
<ol>
<li>s plačilom v celoti oz.je delno v zamudi in Profit Planet GmbH plačuje strankineuspešno določil razumen rok za plačilo neporavnanega zneska, ali</li>
<li>je bila minimalna količina nakupa, dogovorjena v tej pogodbi, v obdobju šestih mesecev nižja za več kot povprečno 20 %, ali</li>
<li>Tuje izdelke so pripravljali na zagotovljenih kavnih avtomatih.</li>
</ol>
</li>
</ol>
<p>(3) V primeru izredne odpovedi brez odpovednega roka s strani družbe Profit Planet GmbH si družba pridržuje pravico, da stranki zaračuna odškodnino za preostalo obdobje v višini 25 % dogovorjene minimalne količine nakupa, kot tudi pogodbeno dogovorjene najemnine. Pravica do zahtevka za nadaljnjo odškodnino ostaja pridržana. Stranka ima pravico dokazati, da ni nastala nobena škoda ali pa je nastala bistveno manjša škoda.</p>
<p><strong>§ 7 Lastništvo</strong></p>
<p>Dobavljeni stroji ostanejo last podjetja Profit Planet GmbH.</p>
<p><strong>§ 8 Notranji sistemi za naročanje in naročila, varstvo podatkov</strong></p>
<p>Poleg tega se strinjam s prejemanjem ekskluzivnih ponudb in informacij, kot sledi:</p>
<p><input type="checkbox" /> Strinjam se, da lahko Profit Planet GmbH obdeluje posredovane podatke in me obvešča o ekskluzivnih ponudbah in drugih informacijah prek e-novic. To soglasje lahko kadar koli prekličem s pošiljanjem e-pošte na [e-poštni naslov manjka]<a href="mailto:.office@profit-planet.com">.office@profit-planet.com</a>.To soglasje je mogoče preklicati. S tem sprejemam politiko zasebnosti.</p>
<p><input type="checkbox" /> Strinjam se, da lahko Profit Planet GmbH obdeluje posredovane podatke in me po telefonu kontaktira glede ekskluzivnih ponudb in drugih informacij. To soglasje lahko kadar koli prekličem po elektronski pošti na naslov <a href="mailto:office@profit-planet.com">office@profit-planet.com</a>. To soglasje je mogoče preklicati. S tem sprejemam politiko zasebnosti.</p>
<p>Predhodno ste nam dali soglasje in že prejemate informacije in ponudbe o naših izdelkih, zdaj pa želite to soglasje preklicati: To soglasje lahko kadar koli prekličete tako, da pošljete e-pošto na naslov <a href="mailto:office@profit-planet.com">office@profit-planet.com</a> biti razveljavljen.</p>
<p><strong>S tem podpisom je/so naprava/e, omenjene na strani 1, sprejete/sprejete pod navedenimi pogoji.</strong></p>
<p>Velja naslednje: <strong>Splošni pogoji poslovanja</strong> pogodbo s podjetjem Profit Planet GmbH. Stranka s tem izrecno izjavlja, da <strong>Splošni pogoji poslovanja</strong> in <strong>Pravilnik o zasebnosti</strong> je prebral/a te pogoje in se z njimi strinja. Pogodba začne veljati z dnem, ko jo sprejme Profit Planet GmbH.<strong> </strong>Pogodba je sklenjena in veljavna pod pogojem pozitivnega kreditnega preverjanja.</p>
<p>Kraj, {{signingCity}} datum: {{currentDate}}</p>
<div class="signature-grid">
<div class="signature-box">
<div class="pp-stamp">{{profitplanetSignature}}</div>
<div>Podpis Profit Planet GmbH</div>
</div>
<div class="signature-box">
<div>{{signatureImage}}</div>
<div>Žig/Podpis stranke</div>
</div>
</div>
<p><em>Informacije o sestavinah, hranilnih vrednostih itd.Najdete ga na domači strani dobavitelja.www.lanaturalifestyle.com ali po telefonu: 0043 552 322 960 na voljo</em>.</p>
<p><strong>Splošni pogoji poslovanja za postrežbo kave</strong></p>
<p><strong>§ 1 Področje uporabe, oblika</strong></p>
<ol>
<li>Ti splošni pogoji poslovanja (GTC) veljajo za vse naše poslovne odnose med našimi strankami in nami. Profit Planet GmbH, Kärntner Straße 227, 8053 Gradec, Avstrija.Ti pogoji poslovanja veljajo za potrošnike in podjetja; v primeru dvoma imajo pred temi pogoji poslovanja prednost obvezne določbe o varstvu potrošnikov (zlasti v skladu z Zakonom o varstvu potrošnikov in Zakonom o prodaji na daljavo). </li>
<li>Ti splošni pogoji poslovanja veljajo zlasti za pogodbe o prodaji in/ali dobavi premičnin (»blago«), ne glede na to, ali blago izdelujemo sami ali ga kupujemo od dobaviteljev. Če ni drugače dogovorjeno, se ti splošni pogoji poslovanja v različici, veljavni v času naročila kupca, ali v vsakem primeru v različici, ki je bila kupcu nazadnje sporočena pisno, uporabljajo kot okvirni sporazum tudi za podobne prihodnje pogodbe, ne da bi se morali nanje v vsakem posameznem primeru ponovno sklicevati.</li>
<li>Veljajo izključno naši splošni pogoji poslovanja. Vsakršni drugačni, nasprotujoči si ali dopolnilni pogoji poslovanja kupca postanejo del pogodbe le, če in v obsegu, v katerem smo se z njihovo veljavnostjo izrecno strinjali. Ta zahteva po izrecnem soglasju velja v vseh primerih, na primer tudi če kupcu dobavimo blago brez zadržkov, čeprav smo seznanjeni s pogoji poslovanja kupca.</li>
<li>Posamezni dogovori, sklenjeni s kupcem v posameznih primerih (vključno s stranskimi dogovori, spremembami in dopolnitvami), imajo vedno prednost pred temi Splošnimi pogoji poslovanja. Za vsebino takih dogovorov je odločilna pisna pogodba ali naša pisna potrditev, razen če se dokaže nasprotno.</li>
<li>Pravno relevantne izjave in obvestila kupca v zvezi s pogodbo (npr. določitev rokov, prijava napak, odstop od pogodbe ali znižanje cene) morajo biti predložene pisno, tj. v pisni ali elektronski obliki (npr. pismo, e-pošta, faks). Zakonske zahteve glede oblike in nadaljnja dokazila, zlasti v primeru dvoma o pooblastilu izjavitelja, ostanejo nespremenjena.</li>
<li>Sklicevanja na uporabljivost zakonskih določb so zgolj za pojasnitev. Tudi brez takšne pojasnitve veljajo zakonske določbe, razen če so v teh pogojih poslovanja neposredno spremenjene ali izrecno izključene.</li>
</ol>
<p><strong>§ 2 Sklenitev pogodbe</strong></p>
<ol>
<li>Naše ponudbe se lahko spremenijo in niso zavezujoče. To velja tudi, če smo kupcu posredovali kataloge, tehnično dokumentacijo (npr. risbe, načrte, izračune, sklicevanja na standarde DIN), druge opise izdelkov ali dokumente tudi v elektronski obliki za katere si pridržujemo lastništvo in avtorske pravice.</li>
<li>Naročilo kupca predstavlja zavezujočo ponudbo za sklenitev pogodbe. Če v naročilu ni drugače navedeno, smo upravičeni, da to ponudbo sprejmemo v 30 dneh od njenega prejema.</li>
<li>Sprejem se lahko izjavi pisno (npr. s potrditvijo naročila) ali z izročitvijo blaga kupcu.</li>
</ol>
<p><strong>§ 3 Dobavni rok in zamuda pri dobavi</strong></p>
<ol>
<li>Dobavni rok se bo dogovoril individualno ali pa ga bomo določili mi ob sprejemu naročila. Če temu ni tako, je dobavni rok približno 1421 dni od sklenitve pogodbe.</li>
<li>Če iz razlogov, na katere nimamo vpliva (nedobavljivost blaga ali storitev), ne bomo mogli izpolniti zavezujočih dobavnih rokov, bomo stranko o tem nemudoma obvestili in ji posredovali nov predvideni datum dobave. Če blago ali storitve tudi v novem dobavnem roku ostanejo nedobavljive, smo upravičeni do celotnega ali delnega odstopa od pogodbe; vsa plačila, ki jih je stranka že opravila, ji bomo takoj povrnili. Nedobavljivost blaga ali storitev v tem smislu vključuje zlasti neizpolnitev obveznosti pravočasne dobave s strani našega dobavitelja, pod pogojem, da smo sklenili skladen posel varovanja pred tveganjem, pri čemer nismo ne mi ne naš dobavitelj krivi in v konkretnem primeru nismo dolžni nabaviti blaga ali storitev.</li>
<li>Našo odgovornost za zamudo pri dobavi urejajo zakonski predpisi. V vsakem primeru je potreben opomin stranke.</li>
</ol>
<p> </p>
<ol>
<li>Pravice stranke in naše zakonske pravice, zlasti v primeru izključitve obveznosti izpolnitve (npr. zaradi nemožnosti ali nerazumnosti izpolnitve in/ali naknadne izpolnitve), ostanejo nespremenjene.</li>
</ol>
<p><strong>§ 4 Dobava, prenos tveganja, prevzem, neizpolnitev prevzema</strong></p>
<ol>
<li>Dobava je franko tovarna, kar je tudi kraj izpolnitve za dobavo in morebitno nadaljnjo izpolnitev. Na zahtevo in stroške kupca bo blago odposlano na drug namembni kraj (prodaja z odpremo). Če ni drugače dogovorjeno, smo upravičeni določiti način odpreme (zlasti prevoznika, pot dostave in embalažo).</li>
<li>Tveganje za naključno izgubo ali naključno poškodbo blaga preide na kupca najkasneje ob dostavi.Pri nakupih po pošti tveganje za nenamerno izgubo ali poškodbo blaga med prevozom preide na kupca ob izročitvi blaga prevozniku/špediterju za podjetja; za potrošnike tveganje preide šele, ko je blago izročeno potrošniku ali tretji osebi, ki jo določi potrošnik (ki ni prevoznik). Če je potrošnik sam uredil prevozno pogodbo brez našega posredovanja, tveganje preide na kupca ob izročitvi blaga prevozniku.</li>
<li>Če kupec ne sprejme naročila, ne sodeluje ali če se naša dobava zamuja iz drugih razlogov, ki jih je mogoče pripisati kupcu, smo upravičeni zahtevati odškodnino za nastalo škodo, vključno z dodatnimi stroški (npr. stroški skladiščenja). Naša pravica do dokazovanja višje škode in naše zakonske pravice (zlasti povračilo dodatnih stroškov, razumno odškodnino in odpoved pogodbe) ostanejo nespremenjene.</li>
</ol>
<p><strong>§ 5 Cene in plačilni pogoji</strong></p>
<ol>
<li>Če ni v posameznih primerih dogovorjeno drugače, veljajo naše cene, veljavne v času sklenitve pogodbe, ex works, plus zakonsko določen DDV.</li>
<li>V primeru nakupov po pošti kupec krije stroške prevoza iz skladišča in stroške morebitnega zavarovanja prevoza, ki ga zahteva kupec. Razen če v posameznem primeru izstavimo račun za dejanske stroške prevoza, se šteje, da je dogovorjena pavšalna cena prevoza (brez zavarovanja prevoza) v višini 200 EUR. Morebitne carine, pristojbine, davke in druge javne dajatve krije kupec.</li>
<li>Kupnina zapade v plačilo v 14 dneh od izstavitve računa in dobave oziroma prevzema blaga. Vendar si tudi v okviru tekočega poslovnega odnosa pridržujemo pravico, da zahtevamo predplačilo za celotno ali delno dobavo. Takšno rezervacijo bomo podali najkasneje ob potrditvi naročila.</li>
<li>Kupec bo v zamudi po izteku zgoraj navedenega plačilnega roka. V času zamude se bodo na kupnino obračunavale obresti po veljavni zakonski obrestni meri zamudnih obresti. Pridržujemo si pravico do uveljavljanja nadaljnje odškodnine zaradi zamude. V odnosu do trgovcev naša pravica do komercialnih zamudnih obresti (§ 352 UGB) ostane nespremenjena.</li>
<li>Kupec je upravičen do pobota ali pridržanja le, če je njegova terjatev pravnomočno uveljavljena ali nesporna. V primeru napak pri dobavi ostanejo kupčeve protizahteve, zlasti tiste v skladu s 7. členom teh splošnih pogojev, nespremenjene.Ta omejitev ne velja za zahtevke zoper potrošnike, ki so pravno povezani z njihovo odgovornostjo.</li>
<li>Če se po sklenitvi pogodbe izkaže, da je naša terjatev do plačila kupnine ogrožena zaradi neplačilne sposobnosti kupca ali grozeče insolventnosti na primer zaradi vložitve predloga za začetek insolventnega postopka ali podobnih okoliščin smo upravičeni zavrniti izpolnitev in kupcu določiti razumen rok za plačilo protiplačila ali zavarovanja. Če ta rok poteče brez rezultata, smo upravičeni do odstopa od pogodbe.</li>
</ol>
<p><strong>§ 6 Pridržek lastništva</strong></p>
<ol>
<li>Lastništvo prodanega blaga si pridržujemo do celotnega poplačila vseh naših trenutnih in prihodnjih terjatev, ki izhajajo iz kupoprodajne pogodbe in tekočega poslovnega razmerja (zavarovane terjatve).</li>
<li>Blago, za katero velja pridržek lastništva, se ne sme zastaviti ali prenesti kot zavarovanje na tretje osebe pred popolnim poplačilom zavarovanih terjatev. Kupec nas mora o tem nemudoma pisno obvestiti, če je vložen predlog za začetek postopka zaradi insolventnosti ali če tretje osebe poskušajo zaseči (ali zapleniti) blago, ki je v naši lasti.</li>
<li>V primeru kršitve pogodbe s strani kupca, zlasti v primeru neplačila kupnine v roku, smo v skladu z zakonskimi določbami upravičeni odstopiti od pogodbe in/ali zahtevati vračilo blaga na podlagi našega pridržka lastništva. Zahteva za vračilo ne pomeni samodejno izjave o odstopu od pogodbe; temveč smo upravičeni zahtevati le vračilo blaga in si pridržujemo pravico do odstopa od pogodbe. Če kupec ne plača kupnine v roku, lahko te pravice uveljavljamo le, če smo mu predhodno brez uspeha določili razumen rok za plačilo ali če določitev takega roka v skladu z zakonskimi določbami ni potrebna.</li>
</ol>
<p>.</p>
<p><strong>§ 7 Bistvene napake</strong></p>
<p>(1) V nujnih primerih, npr. če je ogrožena obratovalna varnost ali če je treba preprečiti nesorazmerno škodo, ima kupec pravico, da napako odpravi sam in od nas zahteva povračilo objektivno potrebnih (razumnih) nastalih stroškov. O takšni samoodpravi nas je treba nemudoma če je mogoče, predhodno obvestiti.Pravica do samoreklamacije ne obstaja, če bi bili upravičeni zavrniti ustrezno naknadno izpolnitev v skladu z zakonskimi določbami.</p>
<p>(2) Če naknadna izpolnitev ni uspešna ali če razumen rok, ki ga je kupec določil za naknadno izpolnitev, poteče brez uspeha ali če naknadna izpolnitev v skladu z zakonskimi določbami ni potrebna, lahko kupec odstopi od kupoprodajne pogodbe ali zniža kupnino. Vendar pa v primeru nepomembne napake ni pravice do odstopa od pogodbe. Ne glede na zgornje določbe veljajo zakonske garancijske pravice za potrošnike brez omejitev. Zlasti garancijski rok za potrošnike je dve leti od dobave blaga.</p>
<p>(3) Kupčeve zahtevke za odškodnino ali povračilo nepotrebnih stroškov obstajajo tudi v primeru napak le v skladu z 8. členom in so sicer izključeni.</p>
<p><strong>§ 8 Druga odgovornost</strong></p>
<p>(1) Razen če ni v teh pogojih poslovanja, vključno z naslednjimi določbami, določeno drugače, odgovarjamo za kršitve pogodbenih in nepogodbenih obveznosti v skladu z zakonskimi predpisi.</p>
<p>(2) Ne glede na pravno podlago odgovarjamo v okviru odgovornosti za krivdo v primerih naklepa in hude malomarnosti. V primerih lahke malomarnosti odgovarjamo le.</p>
<p>a) za škodo, nastalo zaradi poškodbe življenja, telesa ali zdravja,</p>
<p>b) za škodo, ki izhaja iz kršitve bistvenih pogodbenih obveznosti (tj. obveznosti, katerih izpolnitev je bistvena za pravilno izvajanje pogodbe in na katere spoštovanje se pogodbeni partner lahko redno zanese). V tem primeru pa je naša odgovornost omejena na odškodnino za tipično, predvidljivo škodo.</p>
<p>(3) Zgoraj navedene omejitve odgovornosti veljajo tudi v korist tretjih oseb in za kršitve dolžnosti oseb, katerih krivda je v skladu z zakonskimi določbami naša. Ne veljajo, če smo goljufivo prikrili napako ali prevzeli jamstvo za kakovost blaga, niti ne veljajo za zahtevke po Zakonu o odgovornosti za izdelke.</p>
<p>(4) V obsegu, ki ga dovoljuje zakonodaja, ne odgovarjamo za posredno škodo, posledično škodo ali izgubljeni dobiček. Ta izključitev odgovornosti ne velja za potrošnike, če obstaja vzročna zveza s kršitvijo bistvenih pogodbenih obveznosti.</p>
<p>(5) Odstop od pogodbe ali odpoved pogodbe zaradi kršitve pogodbe, ki ne temelji na napaki blaga, je dovoljena le, če smo zanjo odgovorni.Vsaka nadaljnja prosta pravica kupca do odstopa od pogodbe ali odpovedi je izključena v obsegu, ki je zakonsko dovoljen.</p>
<p>(6) Zgornji predpisi ne vplivajo na obvezne določbe Zakona o odgovornosti za izdelke in odgovornost za namerno ali hudo malomarno ravnanje.</p>
<p><strong>§ 9 Zastaralni rok</strong></p>
<p>(1) V pogodbah s podjetji, kot so opredeljene v 1. členu avstrijskega zakona o varstvu potrošnikov (KSchG), se zakonsko določena garancijska doba za premičnine v skladu z 933. členom avstrijskega civilnega zakonika (ABGB) skrajša na eno leto od dobave. To ne velja v primerih goljufivega prikrivanja ali če je bila dana garancija za kakovost blaga.</p>
<p>(2) Skrajšanje roka iz 1. odstavka ne velja za zahtevke kupca za odškodnino, ki je nastala zaradi poškodbe življenja, telesa ali zdravja, v primerih hude malomarnosti ali namernega ravnanja ali za zahtevke v skladu z Zakonom o odgovornosti za izdelke.</p>
<p>(3) Zahtevki za odškodnino zaradi napak (§ 933a ABGB) zastarajo v zakonskem roku treh let od vedenja za škodo in oškodovalca, ne glede na skrajšano garancijsko dobo.</p>
<p>(4) Zakonski garancijski in zastaralni roki veljajo za potrošnike brez omejitev (§§ 922 in naslednji ABGB, § 9 KSchG).</p>
<p><strong>§ 10 Izbira prava in pristojnosti</strong></p>
<p>(1) Vsa pravna razmerja med nami in kupcem so predmet naslednjega:izključno materialno pravo Republike Avstrije, z izjemo Konvencije ZN o pogodbah o mednarodni prodaji blaga (CISG) in drugih mednarodnih kolizijskih pravil, razen če obvezni predpisi o varstvu potrošnikov določajo drugače.</p>
<p>(2) Če je kupec podjetnik v smislu 1. člena avstrijskega zakona o varstvu potrošnikov (KSchG), je za vse spore, ki izhajajo iz te pogodbe ali so z njo povezani, vključno z njeno veljavnostjo in izpolnjevanjem, pristojno sodišče v Gradcu. Vendar pa smo upravičeni tudi do vložitve tožbe na splošnem kraju pristojnosti kupca ali na katerem koli drugem zakonsko dovoljenem kraju pristojnosti.</p>
<p>(3) Za potrošnike veljajo zakonska pravila o pristojnosti. S potrošniki se ne bo sklenil noben odstopni dogovor o pristojnosti.</p>
<p><strong>§ 11 Končne določbe</strong></p>
<p>Če bi katera koli določba te pogodbe bila ali postala neveljavna ali neizvršljiva, to ne vpliva na veljavnost preostalih določb. Stranki se zavezujeta, da bosta vse neveljavne ali neizvršljive določbe nadomestili z novimi določbami, ki v obsegu, ki je pravno dovoljen, odražajo ekonomski namen neveljavnih ali neizvršljivih določb. Enako velja, če se v tej pogodbi ugotovi vrzel. Da bi zapolnili takšno vrzel, se stranki zavezujeta, da si bosta prizadevali za vzpostavitev ustreznih določb v tej pogodbi, ki se bodo čim bolj približale temu, kar bi pogodbeni stranki nameravali v skladu z namenom in namenom te pogodbe, če bi zadevo upoštevali.</p>
<p>Pogoji poslovanja za storitev Profit Planet Coffee, veljavni od 1. avgusta 2025</p>
<table>
<tr>
<td></td>
</tr>
</table>
<p><a id="page4"></a></p>
<p><strong>Informacije za potrošnike o pravici do odstopa od pogodbe (pravica do preklica)</strong><br /></p>
<p><em>Pravica do odstopa od pogodbe:</em>Od te pogodbe imate pravico odstopiti v 14 dneh brez navedbe razloga. Odstopni rok poteče 14 dni od dneva, ko vi ali tretja oseba, ki ni prevoznik in jo določite vi, pridobi fizično posest prvega blaga iz te pogodbe. V primeru pogodbe o storitvah (npr. najem kavnega avtomata) odstopni rok začne teči z dnem sklenitve pogodbe.</p>
<p>Za uveljavljanje pravice do odstopa od pogodbe nas morate (Profit Planet GmbH, Liebenauer Hauptstraße 82c, 8041 Gradec, Avstrija, e-pošta: office@profit-planet.com ) obvestiti o svoji odločitvi o odstopu od te pogodbe z nedvoumno izjavo (npr. s pismom, poslanim po pošti ali elektronski pošti). Uporabite lahko priloženi vzorčni obrazec za odstop od pogodbe, vendar to ni obvezno. Za spoštovanje roka za odstop od pogodbe zadostuje, da nam sporočilo o uveljavljanju pravice do odstopa od pogodbe pošljete pred iztekom roka za odstop od pogodbe.</p>
<p><em>Posledice preklica:</em>Če odstopite od te pogodbe, vam bomo brez nepotrebnega odlašanja in najkasneje v štirinajstih dneh od dneva, ko smo prejeli vaše obvestilo o odstopu od pogodbe, povrnili vsa prejeta plačila vključno s stroški dostave (razen morebitnih dodatnih stroškov, ki bi nastali, če bi izbrali način dostave, ki ni naša najcenejša standardna možnost dostave) povrnili vsa plačila, ki ste jih prejeli od vas vključno s stroški dostave (razen morebitnih dodatnih stroškov, ki bi nastali, če bi izbrali način dostave, ki ni naša najcenejša standardna možnost dostave). Za to povračilo bomo uporabili isti način plačila, kot ste ga uporabili za prvotno transakcijo, razen če ni izrecno dogovorjeno drugače. Za to povračilo vam ne bomo zaračunali nobenih stroškov.</p>
<p>Če je odpovedana pogodba kupoprodajna pogodba za blago, lahko zadržimo vračilo kupnine, dokler ne prejmemo blaga nazaj ali dokler nam ne predložite dokazila o vrnitvi blaga, kar nastopi prej. V tem primeru nam morate blago vrniti ali izročiti brez nepotrebnega odlašanja in v vsakem primeru najpozneje v štirinajstih dneh od dneva, ko nas obvestite o odstopu od te pogodbe. Rok se šteje za izpolnjen, če blago pošljete pred iztekom štirinajstdnevnega roka. Neposredne stroške vračila blaga krijete sami.</p>
<p>Za izgubo vrednosti blaga odgovarjate le, če je ta izguba vrednosti posledica ravnanja z blagom, ki ni potrebno za preizkus njegovega stanja, lastnosti in funkcionalnosti.</p>
<p>Če ste zahtevali, da se storitev (ali redna dobava blaga) začne v roku za odstop od pogodbe, nam morate plačati razumen znesek, ki ustreza deležu storitev, ki so bile že opravljene do trenutka uveljavljanja vaše pravice do odstopa od pogodbe, v primerjavi s celotnim obsegom storitev, predvidenih v pogodbi.</p>
<p><strong>Vzorec obrazca za odpoved</strong><br /></p>
<p>(Če želite preklicati pogodbo, lahko izpolnite ta obrazec in nam ga vrnete.)</p>
<p> Za Profit Planet GmbH, Liebenauer Hauptstraße 82c, 8041 Graz, E-pošta:<a href="mailto:office@profit-planet.com">office@profit-planet.com</a>:</p>
<p> S tem prekličem/prekličemo ()</p>
<p><strong>tisti od mene/nas (</strong>) sklenjena pogodba za nakup naslednjega blaga/opravljanje naslednje storitve</p>
<p> Naročeno dne () / prejeto dne ()</p>
<p> Ime potrošnika/potrošnikov</p>
<p> Naslov potrošnika(-ov)</p>
<p>- Datum</p>
<p> Podpis potrošnika(-ov) (samo za obvestila na papirju)</p>
</div>
</body>
</html>

Binary file not shown.

View File

@ -0,0 +1,264 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Sub-Auftragsverarbeitungs-Vertrag</title>
<style>
@page { size: A4; margin:15mm 12mm 18mm 12mm; }
body { counter-reset:page; font-size:13px; }
h1 { font-size:18px; margin:0 0 6px; }
h2 { font-size:13px; margin:14px 0 6px; }
.page { page-break-after:always; padding:12px 20px 18px; }
.page:last-child { page-break-after:auto; }
.page-header { display:flex; justify-content:flex-end; font-size:0.65em; counter-increment:page; }
.page-header:after { content:"Seite " counter(page); }
.heading-block { text-align:center; margin:0 0 10px; }
.page-header-block{display:flex;flex-direction:column;align-items:flex-end;gap:3px;margin-bottom:4px;}
.print-date{font-size:0.7em;}
.meta-info { border:1px solid #000; padding:8px 12px; margin:12px 0 25px; font-size:0.9em; }
.meta-grid { width:100%; border-collapse:collapse; }
.meta-grid td { padding:3px 6px; vertical-align:top; border-bottom:1px solid #ccc; }
.meta-grid td:first-child { font-weight:bold; width:32%; white-space:nowrap; }
.meta-grid tr:last-child td { border-bottom:0; }
.section { margin-bottom:10px; }
.subsection { margin-left:16px; }
.subsection p { margin:0 0 6px; }
.data-table {
width:100%;
border-collapse:collapse;
border:1px solid #000;
table-layout:fixed;
font-size:0.9em;
margin:0 0 12px;
}
.data-table th,
.data-table td {
border:1px solid #000;
padding:6px 8px;
vertical-align:top;
}
.data-table th {
background:#f5f5f5;
text-align:left;
width:180px;
}
.signatures { display:flex; gap:30px; margin-top:38px; }
.signature { flex:1; text-align:center; }
.sig-block { display:flex; flex-direction:column; align-items:center; gap:6px; min-height:120px; }
.pp-stamp,
.pp-stamp img {
max-width:none !important;
max-height:none !important;
width:auto !important;
height:auto !important;
display:block;
}
@media print {
body {
margin:0;
font-family:Arial,Helvetica,sans-serif;
line-height:1.35;
-webkit-print-color-adjust:exact;
print-color-adjust:exact;
}
}
</style>
</head>
<body style="margin:0;font-family:Arial,Helvetica,sans-serif;line-height:1.35;-webkit-print-color-adjust:exact;print-color-adjust:exact;">
<div class="document" style="margin:0;">
<div class="page" style="page-break-after:auto;">
<div class="page-header-block">
<div class="page-header"></div>
<div class="print-date">Erstellt am: {{currentDate}}</div>
</div>
<div class="heading-block">
<h1>Sub-Auftragsverarbeitungs-Vertrag</h1>
<p style="margin:0 0 6px;">i.S.d. Art. 28 Abs. 3 Datenschutz-Grundverordnung (DS-GVO)</p>
</div>
<p style="margin:0 0 6px;">abgeschlossen zwischen</p>
<p style="margin:0 0 6px;"><strong>Profit Planet GmbH </strong><em>(kurz Auftraggeber)</em><br>
FN 649474i<br>
Kärntner Straße 227<br>
A-8053 Graz</p>
<p style="margin:0 0 6px;">und</p>
<p style="margin:0 0 6px;"><strong>Vertriebspartner </strong>(kurz Auftragnehmer)</p>
<div class="meta-info">
<table class="meta-grid">
<tr>
<td>Firma</td>
<td>{{companyCompanyName}}</td>
</tr>
<tr>
<td>FN</td>
<td>{{companyRegistrationNumber}}</td>
</tr>
<tr>
<td>GF</td>
<td>{{fullName}}</td>
</tr>
<tr>
<td>Geb. am</td>
<td>{{birthDate}}</td>
</tr>
<tr>
<td>Adresse</td>
<td>{{companyAddress}}</td>
</tr>
<tr>
<td>PLZ / Ort</td>
<td>{{companyZipCode}} {{companyCity}}</td>
</tr>
<tr>
<td>Vollständige Adresse</td>
<td>{{companyFullAddress}}</td>
</tr>
<tr>
<td>E-Mail / Telefon</td>
<td>{{email}} / {{phone}}</td>
</tr>
</table>
</div>
<div class="section">
<h2>PRÄAMBEL</h2>
<div class="subsection">
<p>1. Diese Anlage konkretisiert die Verpflichtungen der Vertragsparteien zum Datenschutz, die sich aus der im bestehenden Vertriebspartner-Vertrag („Hauptvertrag“) und seinen Anlagen in ihren Einzelheiten beschriebenen Auftragsverarbeitung ergeben. Sie findet Anwendung auf alle Tätigkeiten, die mit dem Vertrag in Zusammenhang stehen, und bei denen Beschäftigte des Auftragnehmers oder durch den Auftragnehmer Beauftragte personenbezogene Daten („Daten“) des Auftraggebers verarbeiten.</p>
<p>2. Der Auftragnehmer ist sich bewusst, dass der Auftraggeber als Auftragsverarbeiter für Dritte („Verantwortliche“ im Sinne des Art. 4 Nr. 7 DS-GVO) tätig ist. Im Rahmen des vorbezeichneten Hauptvertrags nimmt der Auftraggeber die Dienste des Auftragnehmers als „weiteren Auftragsverarbeiter“ im Sinne von Art. 28 Nr. 4 DS-GVO in Anspruch, um bestimmte Verarbeitungstätigkeiten im Namen des Dritten („Verantwortlicher“ iSd Art. 4 Nr. 7 DS-GVO) auszuführen.</p>
<p>3. Der Auftragnehmer ist sich bewusst, dass der Auftraggeber gegenüber Dritten für die Einhaltung der Pflichten des Auftragnehmers haftet, falls der Auftragnehmer seinen Datenschutzpflichten nach diesem Vertrag und nach dem Gesetz nicht nachkommt.</p>
<p>4. Die Laufzeit dieser Anlage richtet sich nach der Laufzeit des Vertriebspartner-Vertrages, sofern sich aus den Bestimmungen dieser Anlage nicht darüber hinausgehende Verpflichtungen ergeben.</p>
</div>
</div>
<div class="section">
<h2>DAUER, GEGENSTAND UND SPEZIFIZIERUNG DER AUFTRAGSVERARBEITUNG</h2>
<div class="subsection">
<p>1. Alle Daten dürfen nur so lange verarbeitet werden, als das durch die Vertragserfüllung oder den Zweck der Datenverarbeitung erforderlich ist.</p>
<p>2. Aus dem Vertrag ergeben sich Gegenstand und Dauer des Auftrags sowie Art und Zweck der Verarbeitung.</p>
<p>3. Im Einzelnen sind insbesondere die folgenden Daten Bestandteil der Datenverarbeitung:</p>
</div>
</div>
<table class="data-table">
<tr>
<th>Art der Daten</th>
<td>Interessenten- und Kundendaten; Kontaktdaten beim Auftraggeber; Kontaktdaten des jeweiligen Datenverantwortlichen</td>
</tr>
<tr>
<th>Art und Zweck der Datenverarbeitung</th>
<td>Datenerfassung beim Interessenten (potenziellen Kunden); Datenübermittlung (auch elektronisch via E-Mail bzw. falls vorhanden über elektronische Schnittstellen der Verantwortlichen) an Auftraggeber bzw. Datenverantwortliche zur Legung eines Angebots bzw. zur Verwirklichung der Kundenbestellung; ggf. telefonischer Nachkontakt zur Qualitätskontrolle</td>
</tr>
<tr>
<th>Kategorien betroffener Daten</th>
<td>Name, Vorname, Adresse, Geburtsdatum, SV-Nr., E-Mail, Kontodaten, Ausweiskopie; Daten zur Energieversorgung (z.B. Zählpunkt, Zählernummer, Kilowattprognose, Jahresverbrauch); Aufzeichnung etwaiger Qualitätskontrollen; Aufzeichnung etwaiger Interessensgebiete im Bereich Versicherung, Kreditwirtschaft, Telekommunikation, Energieeffizienz (PV, Speicher, LED, Infrarotheizung, Kalkschutz…).</td>
</tr>
</table>
<div class="section">
<h2>ANWENDUNGSBEREICH UND VERANTWORTLICHKEIT</h2>
<div class="subsection">
<p>1. Der Auftragnehmer verarbeitet personenbezogene Daten im Auftrag des Auftraggebers. Dies umfasst Tätigkeiten, die im Vertrag und in der Leistungsbeschreibung konkretisiert sind.</p>
<p>2. Der Auftraggeber ist gegenüber dem/den Dritten als („Verantwortliche Person“ iSd Art. 4 Nr. 7 DS-GVO) für die Einhaltung der gesetzlichen Bestimmungen der Datenschutzgesetze, insbesondere für die Rechtmäßigkeit der Datenweitergabe an den Auftragnehmer sowie für die Rechtmäßigkeit der Datenverarbeitung verantwortlich.</p>
<p>3. Der Auftragnehmer ist gegenüber dem Auftraggeber im Rahmen dieses Vertrages für die Einhaltung der gesetzlichen Bestimmungen der Datenschutzgesetze, insbesondere für die Rechtmäßigkeit der Datenweitergabe sowie der Datenverarbeitung verantwortlich.</p>
<p>4. Die Weisungen werden anfänglich durch diese Vertragsanlage festgelegt und können vom Auftraggeber danach in schriftlicher Form oder in einem elektronischen Format (Textform) an die vom Auftragnehmer bezeichnete Stelle durch einzelne Weisungen geändert, ergänzt oder ersetzt werden (Einzelweisung). Weisungen, die in der Vertragsanlage nicht vorgesehen sind, werden als Antrag auf Leistungsänderung behandelt. Mündliche Weisungen sind unverzüglich schriftlich oder in Textform zu bestätigen.</p>
</div>
</div>
<div class="section">
<h2>PFLICHTEN DES AUFTRAGNEHMERS</h2>
<div class="subsection">
<p>1. Der Auftragnehmer darf Daten von betroffenen Personen nur im Rahmen des Auftrages und der Weisungen des Auftraggebers verarbeiten, außer es liegt ein Ausnahmefall iSd Art 28 Abs. 3 a) DS-GVO vor. Der Auftragnehmer informiert den Auftraggeber unverzüglich, wenn er der Auffassung ist, dass eine Weisung gegen anwendbare Gesetze verstößt. Der Auftragnehmer darf die Umsetzung der Weisung solange aussetzen, bis sie vom Auftraggeber bestätigt oder abgeändert wurde.</p>
<p>2. Der Auftragnehmer wird in seinem Verantwortungsbereich die innerbetriebliche Organisation so gestalten, dass sie den besonderen Anforderungen des Datenschutzes gerecht wird. Er wird technische und organisatorische Maßnahmen zum angemessenen Schutz der Daten des Auftraggebers treffen, die den Anforderungen der Datenschutz- Grundverordnung (Art. 32 DS-GVO) genügen. Der Auftragnehmer hat technische und organisatorische Maßnahmen zu treffen, die die Vertraulichkeit, Integrität, Verfügbarkeit und Belastbarkeit der Systeme und Dienste im Zusammenhang mit der Verarbeitung auf Dauer sicherstellen. Der Auftraggeber ist berechtigt, diese technischen und organisatorischen Maßnahmen dahingehend zu überprüfen, ob sie für die Risiken der zu verarbeitenden Daten ein angemessenes Schutzniveau bieten. Eine Änderung der getroffenen Sicherheitsmaßnahmen bleibt dem Auftragnehmer vorbehalten, wobei jedoch sichergestellt sein muss, dass das vertraglich vereinbarte Schutzniveau nicht unterschritten wird.</p>
<p>3. Der Auftragnehmer gewährleistet, seinen Pflichten nach Art. 32 Abs. 1 lit. d) DS-GVO nachzukommen, ein Verfahren zur regelmäßigen Überprüfung der Wirksamkeit der technischen und organisatorischen Maßnahmen zur Gewährleistung der Sicherheit der Verarbeitung einzusetzen.</p>
<p>4. Der Auftragnehmer unterstützt den Auftraggeber im Rahmen seiner Möglichkeiten bei der Erfüllung der Anfragen und Ansprüche betroffener Personen gem. Kapitel III der DS-GVO sowie bei der Einhaltung der in Art. 33 bis 36 DS-GVO genannten Pflichten.</p>
<p>5. Der Auftragnehmer gewährleistet, dass es den mit der Verarbeitung der Daten des Auftraggebers befassten Mitarbeiter und andere für den Auftragnehmer tätigen Personen untersagt ist, die Daten außerhalb der Weisung zu verarbeiten. Ferner gewährleistet der Auftragnehmer, dass sich die zur Verarbeitung der personenbezogenen Daten befugten Personen zur Vertraulichkeit verpflichtet haben oder einer angemessenen gesetzlichen Verschwiegenheitspflicht unterliegen. Die Vertraulichkeits-/ Verschwiegenheitspflicht besteht auch nach Beendigung des Auftrages fort.</p>
<p>6. Der Auftragnehmer unterrichtet den Auftraggeber unverzüglich, wenn ihm Verletzungen des Schutzes personenbezogener Daten des Auftraggebers bekannt werden. Der Auftragnehmer trifft die erforderlichen Maßnahmen zur Sicherung der Daten und zur Minderung möglicher nachteiliger Folgen der betroffenen Personen und spricht sich hierzu unverzüglich mit dem Auftraggeber ab.</p>
<p>7. Der Auftragnehmer nennt dem Auftraggeber den Ansprechpartner für im Rahmen des Vertrages anfallende Datenschutzfragen.</p>
<p>8. Der Auftragnehmer berichtigt oder löscht die vertragsgegenständlichen Daten, wenn der Auftraggeber dies anweist und dies vom Weisungsrahmen umfasst ist. Ist eine datenschutzkonforme Löschung oder eine entsprechende Einschränkung der Datenverarbeitung nicht möglich, übernimmt der Auftragnehmer die datenschutzkonforme Vernichtung von Datenträgern und sonstigen Materialien auf Grund einer Einzelbeauftragung durch den Auftraggeber oder gibt diese Datenträger an den Auftraggeber zurück, sofern nicht im Vertrag bereits vereinbart.</p>
<p>9. Daten, Datenträger sowie sämtliche sonstige Materialien sind nach Auftragsende auf Verlangen des Auftraggebers entweder herauszugeben oder zu löschen.</p>
<p>10. Im Falle einer Inanspruchnahme des Auftraggebers oder des Dritten durch eine betroffene Person hinsichtlich etwaiger Ansprüche nach Art. 82 DS-GVO, verpflichtet sich der Auftragnehmer den Auftraggeber bei der Abwehr des Anspruches im Rahmen seiner Möglichkeiten zu unterstützen.</p>
<p>11. Im Falle einer Inanspruchnahme des Auftraggebers durch den Dritten, verpflichtet sich der Auftragnehmer den Auftraggeber bei der Abwehr des Anspruches im Rahmen seiner Möglichkeiten zu unterstützen.</p>
</div>
</div>
<div class="section">
<h2>PFLICHTEN DES AUFTRAGGEBERS</h2>
<div class="subsection">
<p>1. Der Auftraggeber hat den Auftragnehmer unverzüglich und vollständig zu informieren, wenn er in den Auftragsergebnissen Fehler oder Unregelmäßigkeiten bzgl. datenschutzrechtlicher Bestimmungen feststellt.</p>
<p>2. Im Falle einer Inanspruchnahme des Auftraggebers oder des Dritten durch eine betroffene Person hinsichtlich etwaiger Ansprüche nach Art. 82 DS-GVO, gilt §3 Abs. 10 entsprechend.</p>
<p>3. Der Auftraggeber nennt dem Auftragnehmer den Ansprechpartner für im Rahmen des Vertrages anfallende Datenschutzfragen.</p>
</div>
</div>
<div class="section">
<h2>ANFRAGEN BETROFFENER PERSONEN</h2>
<div class="subsection">
<p>1. Wendet sich eine betroffene Person mit Forderungen zur Berichtigung, Löschung oder Auskunft an den Auftragnehmer, wird der Auftragnehmer die betroffene Person an den Auftraggeber verweisen und ggf. den Antrag der betroffenen Person unverzüglich an den Auftraggeber weiterleiten. Der Auftragnehmer unterstützt den Auftraggeber im Rahmen seiner Möglichkeiten bei der Erfüllung der jeweiligen Forderung.</p>
<p>2. Der Auftragnehmer haftet nicht, wenn das Ersuchen der betroffenen Person vom Auftraggeber nicht, nicht richtig oder nicht fristgerecht beantwortet wird.</p>
<p>3. Der Auftraggeber haftet nicht für Forderungen betroffener Personen, die dadurch entstehen, dass der Auftragnehmer das entsprechende Anliegen nicht zeitgerecht an den Auftraggeber übermittelt hat.</p>
</div>
</div>
<div class="section">
<h2>NACHWEISMÖGLICHKEITEN</h2>
<div class="subsection">
<p>1. Der Auftragnehmer weist dem Auftraggeber die Einhaltung der in diesem Vertrag niedergelegten Pflichten mit geeigneten Mitteln nach.</p>
<p>2. Sollten im Einzelfall Inspektionen durch den Auftraggeber oder einen von diesem beauftragten Prüfer erforderlich sein, werden diese zu den üblichen Geschäftszeiten ohne Störung des Betriebsablaufs nach Anmeldung unter Berücksichtigung einer angemessenen Vorlaufzeit durchgeführt. Der Auftragnehmer darf diese von der Unterzeichnung einer Verschwiegenheitserklärung hinsichtlich der Daten anderer Kunden und der eingerichteten technischen und organisatorischen Maßnahmen abhängig machen. Sollte der durch den Auftraggeber beauftragte Prüfer in einem Wettbewerbsverhältnis zu dem Auftragnehmer stehen, hat der Auftragnehmer gegen diesen ein Einspruchsrecht.</p>
</div>
</div>
<div class="section">
<h2>SUBUNTERNEHMER (WEITERE AUFTRAGSVERARBEITER)</h2>
<div class="subsection">
<p>1. Der Einsatz von Subunternehmern als weitere Auftragsverarbeiter ist nur zulässig, wenn der Auftraggeber vorher zugestimmt hat.</p>
<p>2. Ein zustimmungspflichtiges Subunternehmerverhältnis liegt vor, wenn der Auftragnehmer weitere Auftragnehmer mit der ganzen oder einer Teilleistung der im Vertrag vereinbarten Leistung beauftragt. Der Auftragnehmer wird mit diesen Dritten im erforderlichen Umfang Vereinbarungen treffen, um angemessene Datenschutz- und Informationssicherheitsmaßnahmen zu gewährleisten.</p>
<p>3. Erteilt der Auftragnehmer Aufträge an Subunternehmer, so obliegt es dem Auftragnehmer, seine datenschutzrechtlichen Pflichten aus diesem Vertrag dem Subunternehmer zu überbinden.</p>
</div>
</div>
<div class="section">
<h2>INFORMATIONSPFLICHTEN, SCHRIFTFORMKLAUSEL, RECHTSWAHL</h2>
<div class="subsection">
<p>1. Sollten die Daten des Auftraggebers beim Auftragnehmer durch Pfändung oder Beschlagnahme, durch ein Insolvenz- oder Vergleichsverfahren oder durch sonstige Ereignisse oder Maßnahmen Dritter gefährdet werden, so hat der Auftragnehmer den Auftraggeber unverzüglich darüber zu informieren. Der Auftragnehmer wird alle in diesem Zusammenhang Verantwortlichen unverzüglich darüber informieren, dass die Hoheit und das Eigentum an den Daten ausschließlich beim Dritten als verantwortliche Person im Sinne der Datenschutz-Grundverordnung liegen.</p>
<p>2. Änderungen und Ergänzungen dieser Anlage und aller ihrer Bestandteile einschließlich etwaiger Zusicherungen des Auftragnehmers bedürfen einer schriftlichen Vereinbarung, die auch in einem elektronischen Format (Textform) erfolgen kann, und des ausdrücklichen Hinweises darauf, dass es sich um eine Änderung bzw. Ergänzung dieser Bedingungen handelt. Dies gilt auch für den Verzicht auf dieses Formerfordernis.</p>
<p>3. Bei etwaigen Widersprüchen gehen Regelungen dieser Anlage zum Datenschutz den Regelungen des Vertrages vor. Sollten einzelne Teile dieser Anlage unwirksam sein, so berührt dies die Wirksamkeit der Anlage im Übrigen nicht.</p>
<p>4. Es gilt das auf dem Hauptvertrag anwendbare Recht sowie Gerichtsstand.</p>
</div>
</div>
<div class="signatures">
<div class="signature">
<p style="margin:0 0 6px;">Für PROFIT PLANET (Auftraggeber)</p>
<div class="sig-block">
<div class="pp-stamp">{{profitplanetSignature}}</div>
<div style="font-size:0.75em;line-height:1.2;">{{currentDate}}</div>
</div>
<p style="margin:0 0 6px;">Datum, Unterschrift</p>
</div>
<div class="signature">
<p style="margin:0 0 6px;">Für den VP (Auftragnehmer)</p>
<div class="sig-block">
<span>{{signatureImage}}</span>
<div style="font-size:0.75em;line-height:1.2;">{{fullName}}</div>
<div style="font-size:0.75em;line-height:1.2;">{{currentDate}}</div>
</div>
<p style="margin:0 0 6px;">Name, Datum, Unterschrift</p>
</div>
</div>
</div>
</div>
</body>
</html>
</ol>
</li>
</ul>
<p><strong>Für PROFIT PLANET </strong>(Auftraggeber)<strong> Für den VP </strong>(Auftragnehmer)</p>
<p>.............................................................................. ...............................................................................</p>
<p>Datum, Unterschrift Name, Datum, Unterschrift </p>
<p> </p>
</div>
</body>
</html>

Binary file not shown.

View File

@ -0,0 +1,294 @@
<!DOCTYPE html>
<html lang="sl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>POGODBA O PODIZVAJALSKI OBDELAVI PODATKOV</title>
<style>
@page { size: A4; margin: 15mm 12mm 18mm 12mm; }
body { counter-reset: page; font-size: 13px; }
h1 { font-size: 18px; margin: 0 0 6px; }
h2 { font-size: 13px; margin: 14px 0 6px; }
.page { page-break-after: always; padding: 12px 20px 18px; }
.page:last-child { page-break-after: auto; }
.page-header {
display: flex;
justify-content: flex-end;
font-size: 0.65em;
counter-increment: page;
}
.page-header:after { content: "Stran " counter(page); }
.heading-block { text-align: center; margin: 0 0 10px; }
.page-header-block {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 3px;
margin-bottom: 4px;
}
.print-date { font-size: 0.7em; }
.meta-info { border: 1px solid #000; padding: 8px 12px; margin: 12px 0 25px; font-size: 0.9em; }
.meta-grid { width: 100%; border-collapse: collapse; }
.meta-grid td { padding: 3px 6px; vertical-align: top; border-bottom: 1px solid #ccc; }
.meta-grid td:first-child { font-weight: bold; width: 32%; white-space: nowrap; }
.meta-grid tr:last-child td { border-bottom: 0; }
.section { margin-bottom: 10px; }
.subsection { margin-left: 16px; }
.subsection p { margin: 0 0 6px; }
.data-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #000;
table-layout: fixed;
font-size: 0.9em;
margin: 0 0 12px;
}
.data-table th,
.data-table td {
border: 1px solid #000;
padding: 6px 8px;
vertical-align: top;
}
.data-table th {
background: #f5f5f5;
text-align: left;
width: 180px;
}
.data-table ul { margin: 0; padding-left: 18px; }
.data-table li { margin: 0 0 4px; }
.signatures { display: flex; gap: 30px; margin-top: 38px; }
.signature { flex: 1; text-align: center; }
.sig-block { display: flex; flex-direction: column; align-items: center; gap: 6px; min-height: 120px; }
.pp-stamp,
.pp-stamp img {
max-width: none !important;
max-height: none !important;
width: auto !important;
height: auto !important;
display: block;
}
@media print {
body {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
line-height: 1.35;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
</style>
</head>
<body style="margin:0;font-family:Arial,Helvetica,sans-serif;line-height:1.35;-webkit-print-color-adjust:exact;print-color-adjust:exact;">
<div class="document" style="margin:0;">
<div class="page" style="page-break-after:auto;">
<div class="page-header-block">
<div class="page-header"></div>
<div class="print-date">Ustvarjeno dne: {{currentDate}}</div>
</div>
<div class="heading-block">
<h1>POGODBA O PODIZVAJALSKI OBDELAVI PODATKOV</h1>
<p style="margin:0 0 6px;">v skladu s členom 28(3) Splošne uredbe o varstvu podatkov (GDPR)</p>
</div>
<p style="margin:0 0 6px;">sklenjena med</p>
<p style="margin:0 0 6px;"><strong>Profit Planet GmbH (v nadaljevanju: naročnik)</strong><br>
FN 649474i<br>
Kärntner Straße 227<br>
A-8053 Graz</p>
<p style="margin:0 0 6px;">in</p>
<p style="margin:0 0 6px;"><strong>Distribucijski partner (v nadaljevanju: izvajalec)</strong></p>
<div class="meta-info">
<table class="meta-grid">
<tr>
<td>Podjetje</td>
<td>{{companyCompanyName}}</td>
</tr>
<tr>
<td>FN</td>
<td>{{companyRegistrationNumber}}</td>
</tr>
<tr>
<td>Direktor</td>
<td>{{fullName}}</td>
</tr>
<tr>
<td>Datum rojstva</td>
<td>{{birthDate}}</td>
</tr>
<tr>
<td>Naslov</td>
<td>{{companyAddress}}</td>
</tr>
<tr>
<td>Poštna številka / kraj</td>
<td>{{companyZipCode}} {{companyCity}}</td>
</tr>
<tr>
<td>Celoten naslov</td>
<td>{{companyFullAddress}}</td>
</tr>
<tr>
<td>E-pošta / telefon</td>
<td>{{email}} / {{phone}}</td>
</tr>
</table>
</div>
<div class="section">
<h2>1. UVODNE DOLOČBE</h2>
<div class="subsection">
<p>1.1. Ta priloga natančneje opredeljuje obveznosti pogodbenih strank glede varstva podatkov, ki izhajajo iz obstoječe pogodbe o distribucijskem partnerstvu ("glavna pogodba") in njenih prilog. Velja za vse dejavnosti, povezane s pogodbo, pri katerih zaposleni izvajalca ali osebe, ki jih je pooblastil izvajalec, obdelujejo osebne podatke ("podatki") naročnika.</p>
<p>1.2. Izvajalec se zaveda, da naročnik deluje kot obdelovalec za tretje osebe ("upravljavci" v smislu člena 4(7) GDPR). V okviru glavne pogodbe naročnik uporablja storitve izvajalca kot "nadaljnjega obdelovalca" v smislu člena 28(4) GDPR.</p>
<p>1.3. Izvajalec se zaveda, da naročnik odgovarja tretjim osebam za spoštovanje obveznosti izvajalca, če izvajalec ne izpolnjuje svojih obveznosti glede varstva podatkov.</p>
<p>1.4. Trajanje te priloge je vezano na trajanje glavne pogodbe, razen če iz te priloge izhajajo dodatne obveznosti.</p>
</div>
</div>
<div class="section">
<h2>2. TRAJANJE, PREDMET IN OPREDELITEV OBDELAVE</h2>
<div class="subsection">
<p>2.1. Podatki se lahko obdelujejo le toliko časa, kolikor je potrebno za izpolnitev pogodbe ali namen obdelave.</p>
<p>2.2. Predmet, trajanje ter vrsta in namen obdelave izhajajo iz pogodbe.</p>
<p>2.3. Zlasti se obdelujejo naslednji podatki:</p>
</div>
</div>
<table class="data-table">
<tr>
<th>Vrsta podatkov</th>
<td>
<ul>
<li>Podatki o interesentih in strankah</li>
<li>Kontaktni podatki</li>
</ul>
</td>
</tr>
<tr>
<th>Namen obdelave</th>
<td>
<ul>
<li>Zbiranje podatkov o potencialnih strankah</li>
<li>Posredovanje podatkov (tudi elektronsko) naročniku ali upravljavcem</li>
<li>Priprava ponudb in izvedba naročil</li>
<li>Telefonski stik za kontrolo kakovosti</li>
</ul>
</td>
</tr>
<tr>
<th>Kategorije posameznikov</th>
<td>
<ul>
<li>Ime, priimek, naslov, datum rojstva, EMŠO, e-mail, bančni podatki</li>
</ul>
</td>
</tr>
<tr>
<th>Dodatni podatki</th>
<td>
<ul>
<li>Kopija osebnega dokumenta</li>
<li>Podatki o energiji (npr. merilno mesto, poraba)</li>
<li>Zapisi kontrol kakovosti</li>
<li>Interesi (zavarovanja, finance, telekomunikacije, energija ipd.)</li>
</ul>
</td>
</tr>
</table>
<div class="section">
<h2>3. PODROČJE UPORABE IN ODGOVORNOST</h2>
<div class="subsection">
<p>3.1. Izvajalec obdeluje podatke v imenu naročnika.</p>
<p>3.2. Naročnik je odgovoren za zakonitost posredovanja podatkov.</p>
<p>3.3. Izvajalec je odgovoren za zakonitost obdelave podatkov.</p>
<p>3.4. Navodila naročnika se lahko pisno ali elektronsko spreminjajo.</p>
</div>
</div>
<div class="section">
<h2>4. OBVEZNOSTI IZVAJALCA</h2>
<div class="subsection">
<p>4.1. Obdelava podatkov je dovoljena samo po navodilih naročnika.</p>
<p>4.2. Izvajalec mora zagotoviti tehnične in organizacijske ukrepe skladno z GDPR (člen 32).</p>
<p>4.3. Izvajalec mora redno preverjati učinkovitost varnostnih ukrepov.</p>
<p>4.4. Izvajalec podpira naročnika pri zahtevah posameznikov.</p>
<p>4.5. Zaposleni so zavezani k zaupnosti.</p>
<p>4.6. Izvajalec mora takoj obvestiti naročnika o kršitvah podatkov.</p>
<p>4.7. Določi kontaktno osebo za varstvo podatkov.</p>
<p>4.8. Podatke popravi ali izbriše po navodilih naročnika.</p>
<p>4.9. Po koncu pogodbe podatke izbriše ali vrne.</p>
<p>4.10. Podpora pri pravnih zahtevkih.</p>
<p>4.11. Podpora naročniku tudi v razmerju do tretjih oseb.</p>
</div>
</div>
<div class="section">
<h2>5. OBVEZNOSTI NAROČNIKA</h2>
<div class="subsection">
<p>5.1. Obveščanje o napakah.</p>
<p>5.2. Pravna odgovornost skladno z GDPR.</p>
<p>5.3. Določitev kontaktne osebe.</p>
</div>
</div>
<div class="section">
<h2>6. ZAHTEVE POSAMEZNIKOV</h2>
<div class="subsection">
<p>6.1. Izvajalec posreduje zahteve naročniku.</p>
<p>6.2. Izvajalec ne odgovarja za napačne odgovore naročnika.</p>
<p>6.3. Naročnik ne odgovarja za zamude izvajalca.</p>
</div>
</div>
<div class="section">
<h2>7. NADZOR</h2>
<div class="subsection">
<p>7.1. Izvajalec mora dokazati skladnost.</p>
<p>7.2. Možne so kontrole ob predhodni najavi.</p>
</div>
</div>
<div class="section">
<h2>8. PODIZVAJALCI</h2>
<div class="subsection">
<p>8.1. Samo z dovoljenjem naročnika.</p>
<p>8.2. Pogodbe z enakimi varnostnimi standardi.</p>
<p>8.3. Prenos obveznosti na podizvajalce.</p>
</div>
</div>
<div class="section">
<h2>9. KONČNE DOLOČBE</h2>
<div class="subsection">
<p>9.1. Obveznost obveščanja o tveganjih.</p>
<p>9.2. Spremembe samo pisno.</p>
<p>9.3. Prednost določb o varstvu podatkov.</p>
<p>9.4. Velja pravo glavne pogodbe.</p>
</div>
</div>
<div class="signatures">
<div class="signature">
<p style="margin:0 0 6px;">Za PROFIT PLANET (naročnik)</p>
<div class="sig-block">
<div class="pp-stamp">{{profitplanetSignature}}</div>
<div style="font-size:0.75em;line-height:1.2;">{{currentDate}}</div>
</div>
<p style="margin:0 0 6px;">Datum, podpis</p>
</div>
<div class="signature">
<p style="margin:0 0 6px;">Za partnerja (izvajalec)</p>
<div class="sig-block">
<span>{{signatureImage}}</span>
<div style="font-size:0.75em;line-height:1.2;">{{fullName}}</div>
<div style="font-size:0.75em;line-height:1.2;">{{currentDate}}</div>
</div>
<p style="margin:0 0 6px;">Datum, podpis</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -319,7 +319,7 @@
Please use the Invoice number as a reference when making the payment.
</p>
</div>
<div class="payment-qr">{{qrCodeImage}}</div>
{% comment %} <div class="payment-qr">{{qrCodeImage}}</div> {% endcomment %}
</div>
</div>

View File

@ -0,0 +1,373 @@
<!doctype html>
<html lang="{{lang}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{documentTitle}} {{invoiceNumber}}</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", Arial, Helvetica, sans-serif;
background: #f4f6fb;
color: #1f2937;
font-size: 13px;
line-height: 1.5;
}
.page {
max-width: 860px;
margin: 28px auto;
background: #ffffff;
border: 1px solid #dbe3f0;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 24px 60px -38px rgba(15, 23, 42, 0.35);
}
.hero {
padding: 34px 38px 28px;
background: linear-gradient(135deg, #1f2937 0%, #0f172a 45%, #0369a1 100%);
color: #ffffff;
}
.hero-grid {
display: flex;
justify-content: space-between;
gap: 24px;
align-items: flex-start;
}
.hero-brand {
display: grid;
gap: 14px;
min-width: 0;
}
.hero-logo:empty {
display: none;
}
.hero-logo img {
display: block;
max-height: 72px;
max-width: 220px;
object-fit: contain;
}
.hero h1 {
margin: 0;
font-size: 31px;
font-weight: 800;
letter-spacing: -0.03em;
}
.hero p {
margin: 8px 0 0;
max-width: 440px;
color: rgba(255, 255, 255, 0.82);
}
.invoice-card {
min-width: 220px;
padding: 18px 20px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.18);
text-align: right;
}
.invoice-card .eyebrow {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.16em;
color: rgba(255, 255, 255, 0.72);
}
.invoice-card .number {
margin-top: 8px;
font-size: 24px;
font-weight: 800;
}
.content {
padding: 30px 38px 36px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
margin-bottom: 24px;
}
.info-card {
padding: 18px;
border: 1px solid #dbe3f0;
border-radius: 18px;
background: #f8fafc;
}
.info-card h2 {
margin: 0 0 10px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.18em;
color: #64748b;
}
.info-card p {
margin: 0;
}
.highlight {
font-weight: 700;
color: #0f172a;
}
.meta-lines {
display: grid;
gap: 6px;
}
.meta-line {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 16px;
min-width: 0;
}
.meta-label {
color: #64748b;
flex: 1 1 auto;
min-width: 0;
padding-right: 12px;
}
.meta-line .highlight,
.meta-line .status-pill {
flex: 0 0 auto;
text-align: right;
}
.status-pill {
display: inline-flex;
align-items: center;
padding: 5px 10px;
border-radius: 999px;
background: #e0f2fe;
color: #0369a1;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.tax-banner {
margin-bottom: 22px;
padding: 15px 18px;
border: 1px solid #bfdbfe;
border-radius: 18px;
background: linear-gradient(180deg, #eff6ff 0%, #f8fafc 100%);
}
.tax-banner-title {
margin: 0 0 6px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.16em;
color: #1d4ed8;
font-weight: 700;
}
.tax-banner p {
margin: 0;
color: #334155;
}
.items-table {
width: 100%;
border-collapse: collapse;
overflow: hidden;
border-radius: 18px;
border: 1px solid #dbe3f0;
margin-bottom: 24px;
}
.items-table thead th {
padding: 12px 14px;
background: #e2e8f0;
color: #334155;
text-align: left;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.14em;
}
.items-table thead th:nth-child(3),
.items-table thead th:nth-child(4),
.items-table thead th:nth-child(5),
.items-table tbody td:nth-child(3),
.items-table tbody td:nth-child(4),
.items-table tbody td:nth-child(5) {
text-align: right;
}
.items-table tbody td {
padding: 12px 14px;
border-top: 1px solid #e2e8f0;
background: #ffffff;
}
.summary-grid {
display: grid;
grid-template-columns: minmax(0, 1.3fr) minmax(280px, 0.9fr);
gap: 20px;
align-items: start;
}
.payment-card,
.totals-card {
padding: 20px;
border: 1px solid #dbe3f0;
border-radius: 18px;
background: #f8fafc;
}
.payment-card h3,
.totals-card h3 {
margin: 0 0 12px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.16em;
color: #64748b;
}
.payment-card p {
margin: 0 0 12px;
color: #334155;
}
.bank-list {
margin-top: 14px;
display: grid;
gap: 8px;
}
.bank-row {
display: flex;
justify-content: space-between;
gap: 16px;
}
.bank-label {
color: #64748b;
}
.bank-value {
text-align: right;
color: #0f172a;
font-weight: 600;
}
.totals-row {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 7px 0;
color: #475569;
}
.totals-row strong {
color: #0f172a;
}
.totals-row.total {
margin-top: 8px;
padding-top: 12px;
border-top: 2px solid #cbd5e1;
font-size: 16px;
font-weight: 800;
color: #0f172a;
}
.footer {
margin-top: 24px;
padding-top: 18px;
border-top: 1px solid #e2e8f0;
color: #64748b;
font-size: 11px;
text-align: center;
line-height: 1.6;
}
@media (max-width: 760px) {
.hero,
.content {
padding-left: 22px;
padding-right: 22px;
}
.hero-grid,
.summary-grid,
.info-grid {
grid-template-columns: 1fr;
display: grid;
}
.invoice-card {
text-align: left;
}
}
</style>
</head>
<body>
<div class="page">
<section class="hero">
<div class="hero-grid">
<div class="hero-brand">
<div class="hero-logo">{{companyLogo}}</div>
<h1>{{documentTitle}}</h1>
</div>
<div class="invoice-card">
<div class="eyebrow">{{invoiceNumberLabel}}</div>
<div class="number">{{invoiceNumber}}</div>
</div>
</div>
</section>
<section class="content">
<div class="info-grid">
<div class="info-card">
<h2>{{fromLabel}}</h2>
<p>
<span class="highlight">{{companyName}}</span><br>
{{companyStreet}}<br>
{{companyPostalCity}}<br>
{{companyCountry}}
</p>
</div>
<div class="info-card">
<h2>{{toLabel}}</h2>
<p>
<span class="highlight">{{customerName}}</span><br>
{{customerEmail}}<br>
{{customerStreet}}<br>
{{customerPostalCity}}<br>
{{customerCountry}}
</p>
</div>
<div class="info-card">
<h2>{{detailsLabel}}</h2>
<div class="meta-lines">
<div class="meta-line"><span class="meta-label">{{dateLabel}}</span><span class="highlight">{{issuedAt}}</span></div>
<div class="meta-line"><span class="meta-label">{{dueDateLabel}}</span><span class="highlight">{{dueAt}}</span></div>
<div class="meta-line"><span class="meta-label">{{statusLabel}}</span><span class="status-pill">{{invoiceStatus}}</span></div>
</div>
</div>
</div>
{{orderedByBlock}}
<div class="tax-banner">
<div class="tax-banner-title">Steuerhinweis</div>
<p><strong>{{taxLabel}}</strong> ({{vatRateDisplay}})</p>
</div>
<table class="items-table">
<thead>
<tr>
<th>#</th>
<th>{{descriptionHeader}}</th>
<th>{{qtyHeader}}</th>
<th>{{unitPriceHeader}}</th>
<th>{{totalHeader}}</th>
</tr>
</thead>
<tbody>
{{itemsRows}}
</tbody>
</table>
<div class="summary-grid">
<div class="payment-card">
<h3>{{paymentInfoTitle}}</h3>
<p>{{paymentInfoText}}</p>
<div class="bank-list">
<div class="bank-row"><span class="bank-label">Kontoinhaber</span><span class="bank-value">{{bankAccountHolder}}</span></div>
<div class="bank-row"><span class="bank-label">IBAN</span><span class="bank-value">{{bankIban}}</span></div>
<div class="bank-row"><span class="bank-label">BIC</span><span class="bank-value">{{bankBic}}</span></div>
</div>
</div>
<div class="totals-card">
<h3>Zusammenfassung</h3>
<div class="totals-row"><span>{{subtotalLabel}}</span><strong>{{totalNet}}</strong></div>
<div class="totals-row"><span>{{taxLabel}} ({{vatRateDisplay}})</span><strong>{{totalTax}}</strong></div>
<div class="totals-row total"><span>{{totalLabel}}</span><span>{{totalGross}}</span></div>
</div>
</div>
<div class="footer">{{footerText}}</div>
</section>
</div>
</body>
</html>

View File

@ -0,0 +1,373 @@
<!doctype html>
<html lang="{{lang}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{documentTitle}} {{invoiceNumber}}</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", Arial, Helvetica, sans-serif;
background: #f4f6fb;
color: #1f2937;
font-size: 13px;
line-height: 1.5;
}
.page {
max-width: 860px;
margin: 28px auto;
background: #ffffff;
border: 1px solid #dbe3f0;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 24px 60px -38px rgba(15, 23, 42, 0.35);
}
.hero {
padding: 34px 38px 28px;
background: linear-gradient(135deg, #102347 0%, #173b73 52%, #2563eb 100%);
color: #ffffff;
}
.hero-grid {
display: flex;
justify-content: space-between;
gap: 24px;
align-items: flex-start;
}
.hero-brand {
display: grid;
gap: 14px;
min-width: 0;
}
.hero-logo:empty {
display: none;
}
.hero-logo img {
display: block;
max-height: 72px;
max-width: 220px;
object-fit: contain;
}
.hero h1 {
margin: 0;
font-size: 31px;
font-weight: 800;
letter-spacing: -0.03em;
}
.hero p {
margin: 8px 0 0;
max-width: 420px;
color: rgba(255, 255, 255, 0.82);
}
.invoice-card {
min-width: 220px;
padding: 18px 20px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.18);
text-align: right;
}
.invoice-card .eyebrow {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.16em;
color: rgba(255, 255, 255, 0.72);
}
.invoice-card .number {
margin-top: 8px;
font-size: 24px;
font-weight: 800;
}
.content {
padding: 30px 38px 36px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
margin-bottom: 24px;
}
.info-card {
padding: 18px;
border: 1px solid #dbe3f0;
border-radius: 18px;
background: #f8fafc;
}
.info-card h2 {
margin: 0 0 10px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.18em;
color: #64748b;
}
.info-card p {
margin: 0;
}
.highlight {
font-weight: 700;
color: #0f172a;
}
.meta-lines {
display: grid;
gap: 6px;
}
.meta-line {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 16px;
min-width: 0;
}
.meta-label {
color: #64748b;
flex: 1 1 auto;
min-width: 0;
padding-right: 12px;
}
.meta-line .highlight,
.meta-line .status-pill {
flex: 0 0 auto;
text-align: right;
}
.status-pill {
display: inline-flex;
align-items: center;
padding: 5px 10px;
border-radius: 999px;
background: #dbeafe;
color: #1d4ed8;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.tax-banner {
margin-bottom: 22px;
padding: 15px 18px;
border: 1px solid #c7d2fe;
border-radius: 18px;
background: linear-gradient(180deg, #eef2ff 0%, #f8fafc 100%);
}
.tax-banner-title {
margin: 0 0 6px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.16em;
color: #4f46e5;
font-weight: 700;
}
.tax-banner p {
margin: 0;
color: #334155;
}
.items-table {
width: 100%;
border-collapse: collapse;
overflow: hidden;
border-radius: 18px;
border: 1px solid #dbe3f0;
margin-bottom: 24px;
}
.items-table thead th {
padding: 12px 14px;
background: #e2e8f0;
color: #334155;
text-align: left;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.14em;
}
.items-table thead th:nth-child(3),
.items-table thead th:nth-child(4),
.items-table thead th:nth-child(5),
.items-table tbody td:nth-child(3),
.items-table tbody td:nth-child(4),
.items-table tbody td:nth-child(5) {
text-align: right;
}
.items-table tbody td {
padding: 12px 14px;
border-top: 1px solid #e2e8f0;
background: #ffffff;
}
.summary-grid {
display: grid;
grid-template-columns: minmax(0, 1.3fr) minmax(280px, 0.9fr);
gap: 20px;
align-items: start;
}
.payment-card,
.totals-card {
padding: 20px;
border: 1px solid #dbe3f0;
border-radius: 18px;
background: #f8fafc;
}
.payment-card h3,
.totals-card h3 {
margin: 0 0 12px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.16em;
color: #64748b;
}
.payment-card p {
margin: 0 0 12px;
color: #334155;
}
.bank-list {
margin-top: 14px;
display: grid;
gap: 8px;
}
.bank-row {
display: flex;
justify-content: space-between;
gap: 16px;
}
.bank-label {
color: #64748b;
}
.bank-value {
text-align: right;
color: #0f172a;
font-weight: 600;
}
.totals-row {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 7px 0;
color: #475569;
}
.totals-row strong {
color: #0f172a;
}
.totals-row.total {
margin-top: 8px;
padding-top: 12px;
border-top: 2px solid #cbd5e1;
font-size: 16px;
font-weight: 800;
color: #0f172a;
}
.footer {
margin-top: 24px;
padding-top: 18px;
border-top: 1px solid #e2e8f0;
color: #64748b;
font-size: 11px;
text-align: center;
line-height: 1.6;
}
@media (max-width: 760px) {
.hero,
.content {
padding-left: 22px;
padding-right: 22px;
}
.hero-grid,
.summary-grid,
.info-grid {
grid-template-columns: 1fr;
display: grid;
}
.invoice-card {
text-align: left;
}
}
</style>
</head>
<body>
<div class="page">
<section class="hero">
<div class="hero-grid">
<div class="hero-brand">
<div class="hero-logo">{{companyLogo}}</div>
<h1>{{documentTitle}}</h1>
</div>
<div class="invoice-card">
<div class="eyebrow">{{invoiceNumberLabel}}</div>
<div class="number">{{invoiceNumber}}</div>
</div>
</div>
</section>
<section class="content">
<div class="info-grid">
<div class="info-card">
<h2>{{fromLabel}}</h2>
<p>
<span class="highlight">{{companyName}}</span><br>
{{companyStreet}}<br>
{{companyPostalCity}}<br>
{{companyCountry}}
</p>
</div>
<div class="info-card">
<h2>{{toLabel}}</h2>
<p>
<span class="highlight">{{customerName}}</span><br>
{{customerEmail}}<br>
{{customerStreet}}<br>
{{customerPostalCity}}<br>
{{customerCountry}}
</p>
</div>
<div class="info-card">
<h2>{{detailsLabel}}</h2>
<div class="meta-lines">
<div class="meta-line"><span class="meta-label">{{dateLabel}}</span><span class="highlight">{{issuedAt}}</span></div>
<div class="meta-line"><span class="meta-label">{{dueDateLabel}}</span><span class="highlight">{{dueAt}}</span></div>
<div class="meta-line"><span class="meta-label">{{statusLabel}}</span><span class="status-pill">{{invoiceStatus}}</span></div>
</div>
</div>
</div>
{{orderedByBlock}}
<div class="tax-banner">
<div class="tax-banner-title">Tax treatment</div>
<p><strong>{{taxLabel}}</strong> ({{vatRateDisplay}})</p>
</div>
<table class="items-table">
<thead>
<tr>
<th>#</th>
<th>{{descriptionHeader}}</th>
<th>{{qtyHeader}}</th>
<th>{{unitPriceHeader}}</th>
<th>{{totalHeader}}</th>
</tr>
</thead>
<tbody>
{{itemsRows}}
</tbody>
</table>
<div class="summary-grid">
<div class="payment-card">
<h3>{{paymentInfoTitle}}</h3>
<p>{{paymentInfoText}}</p>
<div class="bank-list">
<div class="bank-row"><span class="bank-label">Account holder</span><span class="bank-value">{{bankAccountHolder}}</span></div>
<div class="bank-row"><span class="bank-label">IBAN</span><span class="bank-value">{{bankIban}}</span></div>
<div class="bank-row"><span class="bank-label">BIC</span><span class="bank-value">{{bankBic}}</span></div>
</div>
</div>
<div class="totals-card">
<h3>Summary</h3>
<div class="totals-row"><span>{{subtotalLabel}}</span><strong>{{totalNet}}</strong></div>
<div class="totals-row"><span>{{taxLabel}} ({{vatRateDisplay}})</span><strong>{{totalTax}}</strong></div>
<div class="totals-row total"><span>{{totalLabel}}</span><span>{{totalGross}}</span></div>
</div>
</div>
<div class="footer">{{footerText}}</div>
</section>
</div>
</body>
</html>

BIN
templates/vp/vp-DE.docx Normal file

Binary file not shown.

213
templates/vp/vp-DE.html Normal file
View File

@ -0,0 +1,213 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vertriebspartnervertrag</title>
<style>
@page { size:A4; margin:15mm 12mm 18mm 12mm; }
body { font-family:Arial,Helvetica,sans-serif; line-height:1.4; font-size:13px; counter-reset: page; margin:0; }
h1 { text-align:center; font-size:18px; margin:0 0 8px; }
h2 { margin:16px 0 6px; font-size:13px; }
.page { page-break-after:always; padding:12px 20px 18px; }
.page:last-child { page-break-after:auto; }
.page-header { display:flex; justify-content:flex-end; font-size:0.65em; counter-increment:page; }
.page-header:after { content:"Seite " counter(page); }
.meta-info { border:1px solid #000; padding:8px 12px; margin:12px 0 25px; font-size:0.9em; }
.meta-info table { width:100%; border-collapse:collapse; }
.meta-info td { padding:3px 6px; vertical-align:top; border-bottom:1px solid #ccc; }
.meta-info td:first-child { font-weight:bold; width:32%; white-space:nowrap; }
.meta-info tr:last-child td { border-bottom:0; }
.signatures { display:flex; gap:30px; margin-top:38px; }
.signature { flex:1; text-align:center; }
.sig-block { display:flex; flex-direction:column; align-items:center; gap:6px; min-height:120px; }
.page-header-block{display:flex;flex-direction:column;align-items:flex-end;gap:3px;margin-bottom:6px;}
.print-date{font-size:0.7em;}
.pp-stamp,
.pp-stamp img {
max-width:none !important;
max-height:none !important;
width:auto !important;
height:auto !important;
display:block;
}
@media print {
body { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
}
</style>
</head>
<body>
<div class="page">
<div class="page-header-block">
<div class="page-header"></div>
<div class="print-date">Erstellt am: {{currentDate}}</div>
</div>
<h1>Vertriebspartnervertrag</h1>
<p>abgeschlossen zwischen</p>
<p><strong>Profit Planet GmbH </strong><em>(kurz Profit Planet)</em><br>FN 649474i<br>Kärntner Straße 227<br>A-8053 Graz</p>
<p>und</p>
<p><strong>Vertriebspartner </strong>(kurz VP)</p>
<div class="meta-info">
<table class="meta-grid">
<tr>
<td>Firma</td>
<td>{{companyCompanyName}}</td>
</tr>
<tr>
<td>FN</td>
<td>{{companyRegistrationNumber}}</td>
</tr>
<tr>
<td>GF</td>
<td>{{fullName}}</td>
</tr>
<tr>
<td>Geb. am</td>
<td>{{birthDate}}</td>
</tr>
<tr>
<td>Adresse</td>
<td>{{companyFullAddress}}</td>
</tr>
<tr>
<td>E-Mail / Telefon</td>
<td>{{email}} / {{phone}}</td>
</tr>
</table>
</div>
<h2>1. Präambel und Vertragsgegenstand</h2>
<p>1.1 Dieser Vertrag regelt die Zusammenarbeit zwischen PROFIT PLANET und VP als Grundlage einer fairen, langfristigen und erfolgreichen Kooperation. Die VP unterstützen einander im Sinne der Ziele der Zusammenarbeit und unterrichten sich gegenseitig über alle Vorgänge, die für ihre Leistungen im Rahmen der Kooperation von Interesse sind.</p>
<p>1.2 PROFIT PLANET bietet über ein Vertriebspartnernetzwerk den Vertrieb verschiedener Dienstleistungen und Produkte, vornehmlich aus den Bereichen Nachhaltigkeit, Energie, Handel sowie Consulting und Coaching an.</p>
<p>1.3 Der VP vermittelt die jeweiligen Dienstleistungen, Produkte oder qualifizierten Leads, die zu einem Abschluss führen, und erhält dafür eine Provision. Für die Tätigkeit als VP ist es nicht erforderlich, weitere VP zu werben.</p>
<p>1.4 Der VP ist berechtigt, weitere Vertriebspartner für den Vertrieb der Dienstleistungen und Produkte zu gewinnen. Für die Vermittlung und Betreuung der von ihm akquirierten Vertriebspartner erhält der werbende VP eine Provision, die sich aus den erwirtschafteten Umsätzen der geworbenen VP ermittelt. Die Höhe der Provision ergibt sich aus der Provisionsübersicht.</p>
<p>1.5 Die Vertragsabschlüsse kommen nur zwischen dem Endkunden und dem jeweiligen Dienstleister und/oder Produktgeber (Energieversorgungs-, Handels-, Dienstleistungs- oder Coachingunternehmen) zustande, ohne dass dadurch eine Vertragsbeziehung zwischen dem VP und dem Endkunden entsteht. Ein Anspruch auf Abschluss des jeweiligen Vertrags seitens des Endkunden gegenüber PROFIT PLANET oder dem VP entsteht nicht; der Vertragsabschluss ist von der Annahme des entsprechenden Antrags durch den Dienstleister bzw. Produktgeber abhängig. PROFIT PLANET hat darauf keinen Einfluss.</p>
<p>1.6 PROFIT PLANET behält sich vor, die angebotenen Produkte zurückzuziehen, zu ändern, neue hinzuzufügen oder sonstige Anpassungen des Produktangebots vorzunehmen. PROFIT PLANET wird den VP über Änderungen von Produkten oder Tarifen nach Maßgabe der Möglichkeiten rechtzeitig vor Wirksamkeit der Änderungen informieren.</p>
<p>1.7 Die genauen Produktbestandteile und Konditionen ergeben sich aus dem jeweiligen Produktpartnerinformationsblatt, welches auf der Online-Plattform hinterlegt wird.</p>
<p>1.8 PROFIT PLANET ist berechtigt, nach eigenem Ermessen andere Personen und Unternehmen mit der Vermittlung von Produkten und Dienstleistungen von PROFIT PLANET bzw. Produktpartnern von PROFIT PLANET zu beauftragen. Es bestehen grundsätzlich keine Alleinvermittlungsaufträge und keine Exklusivität.</p>
<h2>2. Vertriebspartner werden</h2>
<p>2.1 Kapitalgesellschaften, Personengesellschaften und volljährige natürliche Personen können Vertriebspartner des PROFIT PLANET werden; pro Entität ist die Registrierung nur eines VP-Vertrags vorgesehen. Natürliche Personen, die bloß als Verbraucher handeln (wollen), können nicht Vertriebspartner von PROFIT PLANET werden.</p>
<p>2.2 Kapitalgesellschaften müssen ihrem VP-Antrag die Firmenbuchnummer und gegebenenfalls die Umsatzsteuer- Identifikationsnummer (UID) beilegen. Der Antrag muss von allen Zeichnungsberechtigen der Gesellschaft derart gezeichnet werden, dass eine rechtwirksame Vertretung sichergestellt ist. Die Gesellschafter haften gegenüber PROFIT PLANET jeweils persönlich für das Verhalten der Gesellschaft.</p>
<p>2.3 Absatz 2.2 gilt inhaltsgemäß auch für Personengesellschaften.</p>
<p>2.4 Der VP ist verpflichtet, Änderungen seiner unternehmens- oder personenbezogenen Daten unverzüglich an PROFIT PLANET zu melden.</p>
<p>2.5 Für die Verwendung des Online-Systems gelten die allgemeinen Geschäftsbedingungen.</p>
<p>2.6 PROFIT PLANET kann Vertriebspartner ohne Angabe von Gründen ablehnen.</p>
<h2>3. Leistungen/Pflichten des VP</h2>
<p>3.1 Der VP handelt unabhängig als selbständiger Unternehmer, er ist weder Arbeitnehmer noch Handelsvertreter oder Makler von PROFIT PLANET. Er ist bei der Vermittlung von Produktverträgen eigenverantwortlich tätig, handelt abgesehen von den Pflichten aus diesem Vertrag frei von Weisungen und ist nicht mit der ständigen Vermittlung von Geschäften betraut. Es bestehen seitens PROFIT PLANET keine Umsatzvorgaben und keine Abnahme- oder Vertriebspflichten. Der VP trägt alle mit der Kundenakquisition verbundenen Kosten und Risiken selbst und verwendet eigene Betriebsmittel. Er stellt im geschäftlichen Verkehr klar, dass er nicht im Auftrag oder im Namen von PROFIT PLANET handelt, sondern als unabhängiger Vertriebspartner.</p>
<p>3.2 Der VP betreibt sein Unternehmen mit der Sorgfalt eines ordentlichen Kaufmanns und ist für die Einhaltung aller gesetzlichen sowie der steuer- und sozialrechtlichen Vorgaben selbst verantwortlich.</p>
<p>3.3 Der VP hält sich insbesondere auch an das Wettbewerbsrecht und nimmt Abstand von ungenehmigter, irreführender oder sonst unlauterer Werbung. Der VP verpflichtet sich auch, falsche oder irreführende Aussagen über Dienstleistungen, Produkte und Vertriebssystem der PROFIT PLANET zu unterlassen.</p>
<p>3.4 Grundsätzlich steht es dem VP frei, Produkte/Dienstleistungen auch für andere Unternehmen zu vertreiben. Falls es in der Zusammenarbeit mit einem Dienstleister, Produktgeber oder mit Unterstützung durch PROFIT PLANET zu Terminisierungen, Promotion-Auftritten (POS) oder anderen dienstleistungs- oder produktspezifischen Webetätigkeiten kommt, so kann hierbei insbesondere nach den Vorgaben des jeweiligen Dienstleisters oder Produktgebers untersagt werden, andere Dienstleistungen oder Produkte in räumlicher oder zeitlicher Nähe zu vertreiben.</p>
<p>3.5 Der VP, der andere VP geworben hat, unterstützt die ihm unterstehenden Strukturpartner im Sinne eines funktionalen Teamleiters. Er fungiert als erster Ansprechpartner für Verkaufs-, Produkt- und Provisionsfragen, unterweist die geworbenen VP in ihre Rechte und Pflichten und ist verantwortlich für Trainingsmaßnahmen.</p>
<p>3.6 Der VP verpflichtet sich, für die Beratung und Betreuung von Kunden nur qualifiziertes Fachpersonal einzusetzen.</p>
<p>3.7 Der VP ist verpflichtet, seine Zugangsdaten und Passwörter zum Online-System sicher zu verwahren und vom Zugriff Dritter zu schützen.</p>
<p>3.8 Beim Abschluss von Kundenverträgen ist der VP verpflichtet, die von PROFIT PLANET zur Verfügung gestellten Originalunterlagen (zB Antragsformulare, AGB, sonstige Unterlagen der Dienstleister oder Produktgeber) in der aktuellsten Version zu verwenden und dem Kunden bei Vertragsabschluss vorzulegen bzw. auszuhändigen. Die Originalunterlagen sind durch den VP nicht zu verändern, missbräuchliche Verwendung ist zu verhindern.</p>
<p>3.9 Kundenverträge in Papierform sind vom VP zumindest während der gesetzlichen Aufbewahrungspflichten aufzubewahren und nach Aufforderung durch PROFIT PLANET oder den Produktgeber an PROFIT PLANET auszuhändigen.</p>
<p>3.10 Sämtliche Präsentations-, Werbe- und Schulungsmaterialien sowie label von PROFIT PLANET sind urheberrechtlich geschützt und dürfen ohne ausdrückliches Einverständnis von PROFIT PLANET weder ganz noch teilweise vervielfältigt, verbreitet oder öffentlich zugänglich gemacht werden. Die Herstellung, Verwendung und Verbreitung eigener Werbemittel, Schulungsmaterialien oder Produktbroschüren ist nur nach schriftlicher Genehmigung und Freigabe durch PROFIT PLANET gestattet.</p>
<p>3.11 Der VP ist während der Dauer dieser Vereinbarung und für die Dauer von einem Jahr nach Beendigung dieses Vertrags aus welchem Grund immer, nicht berechtigt, unmittelbar selbst bzw. mittelbar über Dritte Kunden von PROFIT PLANET und ihrer Produktpartner, einschließlich der vom VP vermittelten Endkunden, durch direkte Ansprache abzuwerben. Als Abwerben gilt jede Form des direkten Herantretens an den Kunden mit der Absicht, ihn zum Wechsel zu einem anderen Energieversorgungs-, Dienstleistungs-, Handels-, und/oder Coachingunternehmen zu bewegen (beispielsweise etwa durch Anrufe beim Kunden, Direktmailing mit Absicht der Abwerbung, Haustürgeschäfte etc.).</p>
<h2>4. Geheimhaltung</h2>
<p>4.1 Der VP verpflichtet sich, Geschäfts- und Betriebsgeheimnisse und sonstige vertrauliche Informationen des PROFIT PLANET und dessen Struktur, Geschäftspartner, Vertriebspartner, Produktgeber, Provisionen und Endkunden unter äußerster Geheimhaltung zu behandeln und zu verwahren und diese Daten nur nach erfolgter schriftlicher Zustimmung durch den PROFIT PLANET an Dritte weiterzugeben.</p>
<p>4.2 Diese Verpflichtung gilt auch für Mitarbeiter und Untervertriebspartner des VP. Der VP hat für das Verhalten allfälliger Erfüllungsgehilfen und/oder Subpartner einzustehen.</p>
<p>4.3 Zu den Geschäftsgeheimnissen gehören insbesondere auch Informationen zu internen Betriebsabläufen, Provisionen und Provisionsstrukturen, Produkt- und Preiskalkulationen, Vertriebspartnerstrukturen und -aktivitäten.</p>
<p>4.4 Dem VP ist es nicht gestattet, auf Presseanfragen zu PROFIT PLANET, dessen Provisionspläne, Produkte oder andere Leistungen zu antworten. Presseanfragen sind immer an PROFIT PLANET weiterzuleiten.</p>
<h2>5. Datenschutz</h2>
<p>5.1 Die Vertragspartner sind verpflichtet, die gesetzlichen Datenschutzbestimmungen vollumfänglich einzuhalten. Für Verstöße gegen datenschutzrechtliche Schutzbestimmungen haftet ausschließlich der jeweils die Bestimmung verletzende Vertragspartner, dieser wird den schuldlos handelnden Vertragspartner von allen entsprechenden Ansprüchen freistellen und schad- und klaglos halten.</p>
<p>5.2 Im Regelfall ist der VP ist hinsichtlich der Daten der von ihm vermittelten Endkunden und Akquisitionskontakte Subauftragsverarbeiter im Sinne der Datenschutzgesetze (DSG, DSGVO); PROFIT PLANET ist Auftragsverarbeiter im Sinne der DSGVO. Soweit durch die gesetzlichen Bestimmungen vorgesehen, werden zu dieser Vereinbarung entsprechende datenschutzrechtliche Zusatzverträge abgeschlossen.</p>
<p>5.3 PROFIT PLANET ist bezüglich der Daten des VP auf Datenschutz verpflichtet. Die Datenschutzerklärung ist Online jederzeit abrufbar.</p>
<h2>6. VP-Schutz</h2>
<p>6.1 Ein neu geworbener VP wird in die Struktur desjenigen VP zugewiesen, der ihn geworben hat (VP-Schutz). Wenn mehrere VP denselben VP neu melden, wird seitens PROFIT PLANET nur die zuerst erfolgte Meldung berücksichtigt, wobei das Eingangsdatum des Registrierungsantrags bei PROFIT PLANET für die Zuteilung maßgeblich ist.</p>
<p>6.2 Der meldende VP ist verantwortlich dafür, die Daten des geworbenen VP vollständig und ordentlich zu übermitteln. PROFIT PLANET ist berechtigt, die Daten eines geworbenen VP aus ihrem System zu löschen, wenn von diesem innerhalb einer angemessenen Frist keine Umsätze oder Rückmeldungen kommen.</p>
<p>6.3 Ein Wechsel von der Struktur eines VP in die eines anderen ist grundsätzlich ausgeschlossen und nur ausnahmsweise möglich, wenn der wechselwillige VP nachweist, dass der in der Struktur über ihm stehende VP versucht hat, ihn zu einem gesetzes- oder vertragswidrigen Verhalten zu veranlassen oder sonst schwerwiegende Vorfälle die weitere Zusammenarbeit in der Struktur dieses VP untragbar machen. Über einen entsprechenden schriftlichen Antrag entscheidet PROFIT PLANET nach freiem Ermessen.</p>
<p>6.4 Ein VP, der innerhalb der letzten 12 Monate bereits einen VP-Vertrag mit PROFIT PLANET hatte, kann nicht geworben werden.</p>
<p>6.5 Eine Umgehung des VP-Schutzes etwa durch Verwendung der Namen von Strohnamen, -personen oder -firmen ist untersagt.</p>
<p>6.6 PROFIT PLANET räumt ihren VP ausdrücklich keinen Gebietsschutz ein. Alle VP können europaweit ohne Einschränkungen tätig sein.</p>
<h2>7. Provision</h2>
<p>7.1 Für jedes vom VP erfolgreich vermittelte Vertragsverhältnis zwischen Produktgeber und Endkunden erwirbt der VP Anspruch auf Provision als Bearbeitungs- und Aufwandspauschale.</p>
<p>7.2 Die Höhe der Provision richtet sich nach der jeweils aktuell gültigen Provisionsübersicht laut Marketingkonzept. Die jeweils gültige Fassung dieser Provisionsübersicht ist jederzeit auf der Website von PROFIT PLANET (<a href="http://www.profit-planet.com" target="_new">www.profit-planet.com</a>) im internen Bereich abrufbar, einsehbar, downloadbar und kann dort auch auf Anfrage zur Verfügung gestellt werden. Änderungen der Provisionsübersicht werden dem VP rechtzeitig bekannt gegeben. Es gelten jeweils die zum Zeitpunkt der Vermittlung gültigen Provisionssätze.</p>
<p>7.3 Als erfolgreiche Vermittlung im Sinne dieses Vertrages gilt, wenn das Vertragsverhältnis zwischen Endkunden und Produktpartner tatsächlich zustande gekommen ist. Insbesondere entsteht kein Provisionsanspruch, wenn</p>
<ul>
<li>der Kunde von seinen Widerrufs- oder Rücktrittsrechten Gebrauch macht,</li>
<li>der Vertrag rechtswirksam angefochten wird,</li>
<li>der Kunde vom Dienstleister oder Produktpartner aus welchem Grund auch immer nicht angenommen wird,</li>
<li>fehlerhafte oder unvollständige Kundenanträge eingereicht werden,</li>
<li>der Vertrag widerrechtlich zustande gekommen ist oder</li>
<li>der Dienstleister oder Produktgeber die Auszahlung der Provision an PROFIT PLANET aus Gründen, die nicht von PROFIT PLANET zu verantworten sind, verweigert.</li>
</ul>
<p>7.4 Anspruch auf Auszahlung der Provision entsteht gegenüber PROFIT PLANET grundsätzlich erst dann, wenn die Zahlungen seitens des Geschäftspartners/Produktgebers bei PROFIT PLANET eingelangt sind und alle sonstigen Auszahlungsvoraussetzungen vorliegen. Der VP nimmt zur Kenntnis, dass die exakten Zahlungsmodalitäten bei den verschiedenen Dienstleistern oder Produktgebern voneinander abweichen können und PROFIT PLANET diese Unterschiede bei der Auszahlung berücksichtigt. Die unterschiedlichen Zeitspannen divergieren je nach Partnerunternehmen derzeit durchschnittlich zwischen 30 bis 100 Tage. Die genauen Anforderungen und Konditionen ergeben sich aus dem jeweiligen Produktpartnerinformationsblatt und dem Marketingkonzept.</p>
<p>7.5 Die Auszahlung durch PROFIT PLANET erfolgt einmal monatlich, ungefähr um den 20. des auf den Zahlungseingang bei PROFIT PLANET folgenden Monats. Die Auszahlung erfolgt bargeldlos per Überweisung auf das vom VP genannte Konto. PROFIT PLANET kann Zahlungen bis zu einer Höhe von EUR 100,00 von der Auszahlung ausschließen (Mindestauszahlungshöhe); die nicht ausbezahlten Provisionsansprüche werden auf dem Provisionskonto des VP rechnerisch fortgeführt und im Folgemonat nach Erreichen der Mindestauszahlungshöhe ausbezahlt. Beträge unterhalb der Mindestauszahlungshöhe werden einmal jährlich zur Auszahlung gebracht.</p>
<p>7.6 Der Provisionsanspruch entfällt rückwirkend, wenn PROFIT PLANET, Provisionen an einen Produktgeber zurückzahlen muss, etwa weil ein Kunde den Vertrag widerruft oder andere Ausschlusskriterien seitens des Produktgebers vorliegen (Stornohaftung etc.). PROFIT PLANET ist berechtigt, Forderungen, die dem PROFIT PLANET gegen den VP zustehen, mit dessen Provisionsansprüchen ganz oder teilweise aufzurechnen.</p>
<p>7.7 Mit dieser Provision sind sämtliche Tätigkeiten des VP einschließlich aller ihm in Zusammenhang mit dieser Vereinbarung entstandenen Kosten, Auslagen und Aufwendungen, wie beispielsweise Fahrt- und Reisekosten, Bürokosten, Porto und Telefongebühren, abgegolten. Dasselbe gilt für Leistungen des VP in Hinblick auf Pflege und Herstellung eines VP-Bestandes und/oder Kundenstocks, sodass im Fall der Beendigung des Vertrags unbeachtet des Grundes der Auflösung keinesfalls Ansprüche auf Abfindungen oder Ausgleiche jedweder Art gegen PROFIT PLANET bestehen.</p>
<p>7.8 Fehlerhafte Provisionszahlungen oder sonstige Zahlungen sind vom VP binnen 60 Tagen schriftlich einzumahnen. Danach gelten die Zahlungen als genehmigt.</p>
<p>7.9 Wenn vom VP keine UID-Nummer bekannt gegeben wird, erfolgen alle Auszahlungen netto.</p>
<h2>8. Vertragsstrafe, Schadenersatz</h2>
<p>8.1. Bei einem ersten Verstoß gegen die in diesem Vertrag geregelten Pflichten durch den VP erfolgt eine schriftliche Abmahnung durch PROFIT PLANET. Die Pflichtverletzung ist unmittelbar zu beenden bzw. gegebenenfalls zu beheben.</p>
<p>8.2. Kommt es erneut zu einem Verstoß gegen diesen Vertrag oder wird der zuerst gemahnte Zustand nicht beseitigt, so verpflichtet sich der VP zur Zahlung einer verschuldensunabhängigen Vertragsstrafe für jeden jeweiligen Verstoß in Höhe von EUR 5.000,00.</p>
<p>8.3. Bei Verstößen gegen die Geheimhaltungs- und Datenschutzpflichten, sowie bei besonders schwerwiegenden Verstößen, insbesondere gegen Punkt 10.2 diese Vertrags, ist PROFIT PLANET auch ohne vorhergehende Abmahnung zur Geltendmachung der jeweiligen Vertragsstrafe berechtigt.</p>
<p>8.4. Für jede Zuwiderhandlung gegen Punkt 3.11 verpflichtet sich der VP zur Zahlung einer verschuldens- und schadensunabhängigen Konventionalstrafe an den PROFIT PLANET von EUR 500,00 pro Verstoß (z.B. pro an ein anderes Unternehmen oder sonstigen Dritten vermittelten Vertrags oder pro abgeworbenen Kunden). Die Geltendmachung darüber hinausgehender sonstiger Schadenersatzansprüche, der Vertragsstrafe nach 8.2 oder etwa von Erfüllungsansprüchen bleibt dadurch unberührt.</p>
<p>8.5. Für jeden Verstoß gegen die in Punkt 4. dieses Vertrags (Geheimhaltungsverpflichtung) normierten Pflichten, verpflichtet sich der VP zur Zahlung einer verschuldensunabhängigen Vertragsstrafe in Höhe von EUR 7.000,00 pro Verstoß. Die Geltendmachung weitergehender zivilrechtlicher Ansprüche insbesondere auf Unterlassung und Schadenersatz bleibt davon unberührt.</p>
<p>8.6. Bei Handlungen, die dem Katalog außerordentlicher Kündigungsgründe gemäß Punkt 10.2 entsprechen, insbesondere bei treuwidrigem Verhalten im Sinne der dort beschriebenen Fallgruppen (z. B. unautorisierte Kaltakquise, rufschädigendes Verhalten, unbefugtes Auftreten im Namen von PROFIT PLANET), verpflichtet sich der VP zur Zahlung einer verschuldensunabhängigen Vertragsstrafe in Höhe von EUR 10.000,00 pro Verstoß. Auch in diesen Fällen bleiben darüber hinausgehende Ansprüche insbesondere Schadenersatz oder außerordentliche Kündigung ausdrücklich vorbehalten.</p>
<h2>9. Haftungsausschluss</h2>
<p>9.1. Der VP führt die Vermittlungstätigkeit nach bestem Wissen und Gewissen und in eigener Verantwortung, insbesondere auch in Bezug auf die korrekte Beratung der Endkunden aus. Eine Haftungsübernahme von PROFIT PLANET für Falschberatungen oder sonstiges Fehlverhalten des VP ist explizit ausgeschlossen.</p>
<p>9.2. Für Schäden mit Ausnahme von solchen an Leben, Körper und Gesundheit haftet PROFIT PLANET nur, soweit diese auf Vorsatz oder grober Fahrlässigkeit oder auf schuldhafter Verletzung einer wesentlichen Vertragspflicht (beispielsweise Nichtzahlung der Provision) durch PROFIT PLANET, ihrer Mitarbeiter oder Erfüllungsgehilfen beruhen.</p>
<p>9.3. Eine Haftung von PROFIT PLANET für mittelbare Schäden, Folgeschäden, entgangenen Gewinn oder erwartete Ersparnis ist jedenfalls ausgeschlossen.</p>
<p>9.4. PROFIT PLANET übernimmt keine Haftung für Schäden, die durch Datenverlust auf den Servern auftreten, außer der Schaden beruht auf Vorsatz oder grober Fahrlässigkeit seitens PROFIT PLANET, ihrer Mitarbeiter oder Erfüllungsgehilfen.</p>
<p>9.5. Der Eintritt eines Schadens ist PROFIT PLANET unverzüglich mitzuteilen.</p>
<h2>10. Vertragsdauer &amp; Kündigung</h2>
<p>10.1. Der Vertrag tritt mit Unterzeichnung oder im Fall einer Online-Registrierung, Online mit der Annahme des Vertrags durch PROFIT PLANET in Kraft und wird auf unbestimmte Zeit geschlossen. Er kann von beiden Parteien unter Einhaltung einer Frist von drei Monaten zum Ende jedes Kalendermonats schriftlich gekündigt werden.</p>
<p>10.2. Dessen ungeachtet kann der Vertrag seitens PROFIT PLANET aus wichtigem Grund ohne Einhaltung einer Kündigungsfrist gekündigt werden. Das Recht zur außerordentlichen Kündigung besteht ungeachtet weiterer Ansprüche. Folgende Gründe berechtigen insbesondere zur außerordentlichen Kündigung, die Aufzählung ist nicht abschließend:</p>
<ul>
<li>Akte treuwidrigen Verhaltens, die eine weitere Zusammenarbeit zw. den Vertragspartnern unzumutbar machen;</li>
<li>ein solches treuwidriges Verhalten liegt insbesondere etwa dann vor, wenn ein VP ohne ausdrückliche Zustimmung eines vertretungs- und zeichnungsbefugten Organs von PROFIT PLANET Handlungen setzt, welche nach außen den Anschein erwecken, im Namen oder Auftrag von PROFIT PLANET zu erfolgen insbesondere etwa durch Kaltakquise, Verwendung von Geschäftsdrucksorten oder -signaturen, Auftritt unter Verwendung der Marke PROFIT PLANET oder vergleichbare ruf- bzw. imageschädigende Aktivitäten.</li>
<li>die Anwendung unlauterer Praktiken oder ein grober oder wiederholter Verstoß gegen diesen Vertrag sowie der Verstoß gegen zwingende Rechtsnormen;</li>
<li>wenn über das Vermögen des jeweils anderen Vertragspartners die Einleitung eines Insolvenzverfahrens beantragt oder wenn die Eröffnung eines Insolvenzverfahrens mangels Masse abgelehnt wird;</li>
<li>Verletzung der vereinbarten oder gesetzlichen Datenschutz- oder Geheimhaltungspflichten;</li>
<li>wenn die Kooperation durch das Verhalten eines Vertragspartners oder dessen Ruf in der Öffentlichkeit den anderen Vertragspartnern einen Imageschaden zufügen würde;</li>
<li>wenn die Kooperation aufgrund der Gesetzeslage oder von dritter Seite als unzulässig untersagt wird;</li>
<li>Unzulässige Nebenabsprachen mit am Vertrieb beteiligten Dritten;</li>
</ul>
<p>10.3. Abgesehen von 10.2 kann PROFIT PLANET den VP auch außerordentlich kündigen, wenn dieser in den letzten 6 Monaten keine neuen Umsätze erzielt hat oder bei den durch seine Vermittlung zustande gekommenen Verträgen zwischen Endkunden und Produktgebern über einen Zeitraum von 2 Monaten überdurchschnittliche Stornoquoten von mehr als 30% der vermittelten Verträge bestehen. PROFIT PLANET wird den VP vor einer außerordentlichen Kündigung nach diesem Passus einmalig schriftlich verwarnen, so dass der VP die Möglichkeit hat, innerhalb einer Frist von 30 Tagen die erforderlichen neuen Umsätze zu generieren oder seine Stornoquote zu verbessern.</p>
<p>10.4. Mit der Beendigung des Vertrags steht dem VP mit Ausnahme der Provision für zu diesem Zeitpunkt bereits erfolgreich vermittelte Verträge, kein Recht auf Provision mehr zu. Ein Anspruch auf Handelsvertreterausgleich ist ausdrücklich ausgeschlossen, da der VP nicht als Handelsvertreter für den PROFIT PLANET tätig wird. Etwaige Ansprüche auf Folgeprovisionen für vermittelte Produkte bestehen für 12 Monate nach Vertragsbeendigung fort; im Falle einer außerordentlichen Kündigung verfallen Ansprüche auf Folgeprovisionen unmittelbar mit der Vertragsbeendigung.</p>
<p>10.5. Nach Beendigung des Vertrags sind vom VP sämtliche überlassenen Unterlagen und Werbematerialien unaufgefordert binnen einem Monat an PROFIT PLANET zurückzugeben. Die Verwendung der Marke PROFIT PLANET und entsprechender Logos etwa auf Briefpapier oder in E-Mail-Signaturen ist nach Beendigung des Vertrags untersagt.</p>
<h2>11. Übertragung</h2>
<p>11.1. PROFIT PLANET ist jederzeit berechtigt, den Geschäftsbetrieb ganz oder teilweise auf Dritte zu übertragen, sofern sich der Rechtsnachfolger an die gesetzlichen Vorschriften und die geltenden Verträge hält.</p>
<p>11.2. Der VP ist grundsätzlich berechtigt, seine Vertriebsstruktur an einen Dritten zu übertragen, wobei PROFIT PLANET ausdrücklich ein Vorkaufsrecht vorbehalten ist. Die Übertragung ist PROFIT PLANET im Vorhinein anzuzeigen und der Kauf- und/oder Übertragungsvertrag mit dem Dritten sowie der VP-Antrag des Dritten vorzulegen. Sofern PROFIT PLANET von seinem Vorkaufsrecht binnen einem Monat nach Anzeige der Übertragungsabsicht keinen Gebrauch macht und auch sonst keinen wichtigen Ablehnungsgrund nennt, gilt die Übertragung als durch PROFIT PLANET gebilligt. Ein Verkauf ist jedenfalls ausgeschlossen, wenn der Vertriebspartnervertrag bereits ordentlich oder außerordentlich gekündigt wurde oder der verkaufende VP noch Schulden bei PROFIT PLANET hat.</p>
<p>11.3. Wenn eine als VP registrierte Kapital- oder Personengesellschaft einen neuen Gesellschafter aufnimmt, hat dies auf diesen Vertrag keine Auswirkung, sofern der/die Gesellschafter, die den VP-Antrag ursprünglich unterzeichnet haben, als Gesellschafter in der Gesellschaft verbleiben. Wenn ein Gesellschafter aus einer registrierten Gesellschaft ausscheidet oder seine Anteile an einen Dritten überträgt, so ist dies in Bezug auf diesen Vertrag zulässig, sofern er dies PROFIT PLANET schriftlich unter Vorlage der entsprechenden rechtsgültigen Urkunden anzeigt, und der Vorgang keinen anderen Bestimmungen dieses Vertrags widerspricht; anderenfalls behält PROFIT PLANET sich das Recht vor, den VP-Vertrag der betreffenden Kapital- oder Personengesellschaft aufzukündigen.</p>
<p>11.4. Bei Auflösung einer als VP registrierten Gemeinschaft (Kapital- oder Personengesellschaft, aber auch z.B. Ehepartnerschaften oder ähnliches, die einen gemeinsamen VP-Vertrag haben), bleibt nur ein VP-Vertrag bestehen. Die Mitglieder der aufzulösenden Gemeinschaft haben sich intern zu einigen, durch welches Mitglied/Gesellschafter die Vertriebspartnerschaft fortgesetzt werden soll, und dies PROFIT PLANET schriftlich anzuzeigen. Falls sich die Mitglieder der Gemeinschaft in Bezug auf die Fortsetzung des VP-vertrags nicht gütlich einigen können, behält sich PROFIT PLANET das Recht einer außerordentlichen Kündigung vor, insbesondere, wenn es durch die Uneinigkeit über die Folgen zur Vernachlässigung der Pflichten des VP, einem Verstoß gegen diesen Vertrag oder geltendes Recht oder zu einer übermäßigen Belastung der Vertriebsstruktur des VP kommt.</p>
<h2>12. Schlussbestimmungen</h2>
<p>12.1. Änderungen und Ergänzungen dieser Vereinbarung bedürfen der Schriftform. Dies gilt auch für das Abgehen der Schriftformerfordernis. Mündliche Nebenabreden bestehen nicht.</p>
<p>12.2. Sollte eine Bestimmung dieser Vereinbarung unwirksam sein oder werden, gilt anstelle der unwirksamen Bestimmung jene Bestimmung als vereinbart, die dem wirtschaftlichen Zweck der unwirksamen Bestimmung am nächsten kommt.</p>
<p>12.3. Vereinbarter Gerichtsstand für alle Streitigkeiten aus oder in Zusammenhang mit dieser Vereinbarung ist das für Graz sachlich zuständige Gericht. Diese Vereinbarung unterliegt österreichischem Recht, nicht jedoch den nichtzwingenden Verweisungsnormen des IPR. Weiter- bzw. Rückverweisungen sind ausgeschlossen. Darüber hinaus steht es PROFIT PLANET frei, den VP auch seinem allgemeinen Gerichtsstand zu klagen.</p>
<div class="signatures">
<div class="signature" style="flex:1;text-align:center;">
<p style="margin:0 0 6px;">Für PROFIT PLANET</p>
<div class="sig-block" style="display:flex;flex-direction:column;align-items:center;gap:6px;min-height:140px;">
<div class="pp-stamp" style="display:block;max-width:220px;margin:0 auto 6px;">{{profitplanetSignature}}</div>
<div style="font-size:0.75em;line-height:1.2;">{{currentDate}}</div>
</div>
<p style="margin:0 0 6px;">Datum, Unterschrift</p>
</div>
<div class="signature">
<p style="margin:0 0 6px;">Für den VP</p>
<div class="sig-block">
<span>{{signatureImage}}</span>
<div style="font-size:0.75em;line-height:1.2;">{{fullName}}</div>
<div style="font-size:0.75em;line-height:1.2;">{{currentDate}}</div>
</div>
<p style="margin:0 0 6px;">Name, Datum, Unterschrift</p>
</div>
</div>
</div>
</body>
</html>

BIN
templates/vp/vp-SL.docx Normal file

Binary file not shown.

212
templates/vp/vp-SL.html Normal file
View File

@ -0,0 +1,212 @@
<!DOCTYPE html>
<html lang="sl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pogodba s Prodajnimi partnerji / Poslovnimi partnerji / Povezanimi partnerji</title>
<style>
@page { size:A4; margin:15mm 12mm 18mm 12mm; }
body { font-family:Arial,Helvetica,sans-serif; line-height:1.4; font-size:13px; counter-reset: page; margin:0; }
h1 { text-align:center; font-size:18px; margin:0 0 8px; }
h2 { margin:16px 0 6px; font-size:13px; }
.page { page-break-after:always; padding:12px 20px 18px; }
.page:last-child { page-break-after:auto; }
.page-header { display:flex; justify-content:flex-end; font-size:0.65em; counter-increment:page; }
.page-header:after { content:"Stran " counter(page); }
.meta-info { border:1px solid #000; padding:8px 12px; margin:12px 0 25px; font-size:0.9em; }
.meta-info table { width:100%; border-collapse:collapse; }
.meta-info td { padding:3px 6px; vertical-align:top; border-bottom:1px solid #ccc; }
.meta-info td:first-child { font-weight:bold; width:32%; white-space:nowrap; }
.meta-info tr:last-child td { border-bottom:0; }
.signatures { display:flex; gap:30px; margin-top:38px; }
.signature { flex:1; text-align:center; }
.sig-block { display:flex; flex-direction:column; align-items:center; gap:6px; min-height:120px; }
.page-header-block{display:flex;flex-direction:column;align-items:flex-end;gap:3px;margin-bottom:6px;}
.print-date{font-size:0.7em;}
.pp-stamp,
.pp-stamp img {
max-width:none !important;
max-height:none !important;
width:auto !important;
height:auto !important;
display:block;
}
@media print {
body { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
}
</style>
</head>
<body>
<div class="page">
<div class="page-header-block">
<div class="page-header"></div>
<div class="print-date">Ustvarjeno dne: {{currentDate}}</div>
</div>
<h1>pogodba</h1>
<p style="text-align:center;margin:0 0 6px;">s Prodajnimi partnerji / Poslovnimi partnerji / Povezanimi partnerji -</p>
<p style="text-align:center;margin:0 0 6px;">Z zadnji veljavni različici z dne 21. maja 2025</p>
<p>sklenjena med</p>
<p><strong>Profit Planet GmbH </strong><em>(krajše PROFIT PLANET)</em><br>FN 649474i<br>Kärntner Straße 227<br>A-8041 Graz</p>
<p>in</p>
<p><strong>Prodajni partner/poslovni partner/povezani partner </strong>(<em>krajše</em> <em>PARTNER</em>)</p>
<div class="meta-info">
<table class="meta-grid">
<tr>
<td>Podjetje</td>
<td>{{companyCompanyName}}</td>
</tr>
<tr>
<td>Matična številka</td>
<td>{{companyRegistrationNumber}}</td>
</tr>
<tr>
<td>Identifikacijska št.</td>
<td>{{vatNumber}}</td>
</tr>
<tr>
<td>Rojen dne</td>
<td>{{birthDate}}</td>
</tr>
<tr>
<td>Naslov</td>
<td>{{companyFullAddress}}</td>
</tr>
<tr>
<td>E-pošta / telefon</td>
<td>{{email}} / {{phone}}</td>
</tr>
</table>
</div>
<h2>1. UVOD IN PREDMET POGODBE</h2>
<p>1.1 Ta pogodba ureja sodelovanje med PROFIT PLANET in PARTNER kot podlago za pošteno, dolgoročno in uspešno sodelovanje. PARTNER oz. partnerji podpirajo drug drugega glede ciljev sodelovanja in se medsebojno obveščajo o vseh postopkih, ki so pomembni za njune storitve v okviru sodelovanja.</p>
<p>1.2 PROFIT PLANET prek mreže prodajnih partnerjev/poslovnih partnerjev/povezanih partnerjev ponuja prodajo različnih storitev in izdelkov, predvsem s področij trajnosti, energije, trgovine ter svetovanja in coachinga.</p>
<p>1.3 PARTNER posreduje posamezne storitve, izdelke ali kvalificirane kontakte (lead-e), ki vodijo do sklenitve pogodb, in za to prejme provizijo. Za delovanje kot PARTNER ni potrebno, da PARTNER pridobiva druge PARTNERJE.</p>
<p>1.4 PARTNER ima pravico pridobivati druge prodajne/poslovne/povezane partnerje za prodajo storitev in izdelkov. Za pridobitev in podporo PARTNERJEV, ki jih je sam pridobil, pridobitelj prejme provizijo, ki se izračuna na podlagi ustvarjenih prihodkov pridobljenih PARTNERJEV. Višina provizije izhaja iz preglednice provizij.</p>
<p>1.5 Pogodbe se sklepajo izključno med končnim kupcem in posameznim ponudnikom storitev in/ali izdelkov (npr. podjetje za oskrbo z energijo, trgovsko podjetje, podjetje za storitve ali coaching), brez da bi s tem nastalo pogodbeno razmerje med PARTNERJEM in končnim kupcem. Pravice za sklenitev pogodb s strani končnega kupca zoper PROFIT PLANET ali PARTNERJA ne obstajajo; sklenitev pogodbe je odvisna od sprejema ustrezne ponudbe s strani ponudnika storitev ali izdelkov. PROFIT PLANET na to nima vpliva.</p>
<p>1.6 PROFIT PLANET si pridržuje pravico, da umakne ponujene izdelke, jih spremeni, doda nove ali kako drugače prilagodi ponudbo izdelkov. PROFIT PLANET bo PARTNERJA pravočasno, skladno z možnostmi, obvestil o spremembah izdelkov ali tarif pred uveljavitvijo sprememb.</p>
<p>1.7 Natančne sestavine/sestave izdelkov in pogoji izhajajo iz posameznega informacijskega lista o partnerskih izdelkih, ki je objavljen na spletni platformi.</p>
<p>1.8 PROFIT PLANET je po lastni presoji upravičen, da pooblasti druge osebe in podjetja za posredovanje izdelkov in storitev PROFIT PLANET oz. partnerskih ponudnikov PROFIT PLANET. Načeloma ne obstajajo ekskluzivne posredniške pravice in nobena ekskluzivnost.</p>
<h2>2. POSTATI PRODAJNI / POSLOVNI / POVEZANI PARTNER</h2>
<p>2.1 Kapitalske družbe, osebne družbe in polnoletne fizične osebe lahko postanejo prodajni/poslovni/povezani partnerji PROFIT PLANET; na posamezni subjekt je predvidena registracija vedno le ene partnerske pogodbe za posameznega PARTNERJA. Fizične osebe, ki delujejo (želijo delovati) le kot potrošniki, ne morejo postati poslovni partnerji PROFIT PLANET.</p>
<p>2.2 Kapitalske družbe morajo svoji vlogi za PARTNERSTVO priložiti matično številko in po potrebi identifikacijsko številko za DDV. Vloga mora biti podpisana s strani vseh zakonitih zastopnikov, tako da je zagotovljeno veljavno zastopanje. Družbeniki kapitalskih družb odgovarjajo napram PROFIT PLANET-u osebno za ravnanje svojih družb.</p>
<p>2.3 Določilo iz odstavka 2.2 se smiselno uporablja tudi za osebne družbe.</p>
<p>2.4 PARTNER je dolžan nemudoma sporočiti spremembe svojih poslovnih ali osebnih podatkov PROFIT PLANET-u.</p>
<p>2.5 Za uporabo spletnega sistema veljajo Splošni pogoji poslovanja.</p>
<p>2.6 PROFIT PLANET lahko brez navedbe razlogov zavrne prodajne/poslovne/povezane partnerje.</p>
<h2>3. STORITVE IN OBVEZNOSTI PARTNERJA</h2>
<p>3.1 PARTNER deluje neodvisno kot samostojni podjetnik; ni zaposlen, ni komercialni zastopnik ali posrednik pri PROFIT PLANET-u. Pri posredovanju produktnih pogodb deluje samostojno, ravna prosto brez navodil - razen obveznosti iz te pogodbe - in ni stalno zadolžen za posredovanje poslov. PROFIT PLANET ne postavlja prodajnih ciljev, obveznosti nabave ali distribucije. PARTNER nosi vse stroške in tveganja, povezana s pridobivanjem strank, ter uporablja lastna sredstva. V poslovnih stikih mora jasno komunicirati, da ne deluje v imenu PROFIT PLANET, temveč kot neodvisni prodajni/poslovni/povezani partner.</p>
<p>3.2 PARTNER vodi svoje podjetje z dolžno skrbnostjo dobrega poslovneža in je sam odgovoren za spoštovanje vseh zakonskih, davčnih in socialnih predpisov.</p>
<p>3.3 PARTNER se zlasti drži konkurenčnega prava in se vzdrži nepooblaščenega, zavajajočega ali drugače nepoštenega oglaševanja. Prav tako se zavezuje, da ne bo dajal lažnih ali zavajajočih izjav o storitvah, izdelkih ali prodajnem sistemu PROFIT PLANETa.</p>
<p>3.4 Načeloma ima PARTNER svobodo, da prodaja izdelke ali storitve tudi za druga podjetja. Če pa pride do prekrivanja v prodaji z drugim ponudnikom storitev ali izdelkov, zlasti pri določanju terminov, promocijah na prodajnih mestih ali drugih promocijskih nastopih (POS), je to dovoljeno le z izrecnim soglasjem PROFIT PLANETa.</p>
<p>3.5 Pri sklepanju pogodb s strankami je PARTNER dolžan uporabljati originalne dokumente (npr. obrazce, splošne pogoje, druge dokumente ponudnika), ki jih zagotavlja PROFIT PLANET, v njihovi trenutno veljavni različici ter jih je dolžan strankam ob sklenitvi pogodbe predstaviti ali izročiti. Dokumentov ne sme samovoljno spreminjati ali zlorabljati.</p>
<p>3.6 Pogodbe s strankami, ki so sklenjene v papirni obliki, mora PARTNER nemudoma, najkasneje v enem tednu po pozivu PROFIT PLANET-a ali ponudnika izdelka, predati PROFIT PLANET-u.</p>
<p>3.7 Vsi predstavitveni, oglaševalski in izobraževalni materiali ter blagovne znamke PROFIT PLANET-a so avtorsko zaščiteni in jih brez izrecnega soglasja PROFIT PLANET-a ni dovoljeno razmnoževati, razširjati ali javno objavljati. Izdelava, uporaba in razdeljevanje lastnega oglaševalskega materiala, izobraževalnega gradiva ali brošur o produktih so dovoljeni le s pisnim soglasjem PROFIT PLANET-a.</p>
<p>3.8 PARTNER med trajanjem te pogodbe in še 36 mesecev po njeni prekinitvi nima pravice, da neposredno ali posredno prek tretjih oseb nagovarja stranke PROFIT PLANET-a ali njihovih partnerskih ponudnikov, tudi ne lastnih pridobljenih strank, z namenom, da bi jih pridobil za drugo podjetje s področja energije, storitev, trgovine ali coachinga (npr. prek klicev, direktne pošte ali obiskov na domu).</p>
<h2>4. ZAUPNOST</h2>
<p>4.1 PARTNER se zavezuje, da bo poslovne in obratovalne skrivnosti ter druge zaupne informacije PROFIT PLANET-a in njegovih struktur, prodajnih/poslovnih/povezanih partnerjev, ponudnikov in končnih strank varoval z najvišjo stopnjo zaupnosti ter jih hranil in posredoval tretjim osebam samo s pisnim soglasjem PROFIT PLANET-a.</p>
<p>4.2 Ta obveznost velja tudi za zaposlene in pod- prodajne/pod-poslovne/pod-povezane partnerje PARTNER-ja. PARTNER je odgovoren za ravnanje svojih izvajalcev in/ali podizvajalcev.</p>
<p>4.3 Med poslovne skrivnosti sodijo zlasti informacije o notranjih poslovnih postopkih, provizijah in provizijskih strukturah, kalkulacijah izdelkov in cen, strukturah in dejavnostih prodajnih/poslovnih/povezanih partnerjev.</p>
<p>4.4 PARTNER-ju ni dovoljeno odgovarjati na novinarska vprašanja o PROFIT PLANET-u, njegovih provizijskih načrtih, izdelkih ali drugih storitvah. Vsa novinarska vprašanja je treba vedno posredovati PROFIT PLANET-u.</p>
<h2>5. VARSTVO PODATKOV</h2>
<p>5.1 Pogodbeni stranki sta dolžni v celoti spoštovati zakonske določbe o varstvu podatkov. Za kršitve določb varstva podatkov odgovarja izključno pogodbeni partner, ki je kršil določbo, ta pa bo drugega pogodbenega partnerja, ki je ravnal brez krivde, obvaroval vseh ustreznih zahtevkov in ga zaščitil pred tožbami.</p>
<p>5.2 Na splošno velja, da PARTNER - glede podatkov o posredovanih končnih strankah in kontaktih za akvizicijo - nastopa kot podizvajalec v smislu zakonodaje o varstvu podatkov (DSG, Splošna uredba o varstvu podatkov GDPR); PROFIT PLANET je izvajalec v smislu zakona GDPR. Če je to določeno z zakonskimi določbami, se k tej pogodbi sklenejo ustrezne dodatne pogodbe o varstvu podatkov.</p>
<p>5.3 PROFIT PLANET je glede podatkov PARTNER-ja prav tako zavezan k varstvu podatkov. Izjava o varstvu podatkov je vedno dostopna na spletu.</p>
<h2>6. ZAŠČITA PARTNER-ja</h2>
<p>6.1 Novo pridobljen PARTNER bo dodeljen v strukturo tistega PARTNER-ja, ki ga je pridobil (zaščita PARTNER-ja). Če več PARTNER-jev prijavi istega novega PARTNER-ja, se upošteva zgolj prvi prejeti obrazec za prijavo pri PROFIT PLANET-u; pri tem je odločilen datum prejema prijave.</p>
<p>6.2 PARTNER, ki prijavlja novega PARTNER-ja, je odgovoren za popolno in pravilno posredovanje podatkov. PROFIT PLANET ima pravico izbrisati podatke PARTNER-ja iz svojega sistema, če ta v razumnem roku ne ustvari prihodkov ali povratnih informacij.</p>
<p>6.3 Prehod iz strukture enega PARTNERJA v strukturo drugega je na splošno izključen in mogoč le v izjemnih primerih, če PARTNER, ki želi prestopiti, dokaže, da ga je PARTNER, ki je nad njim v strukturi, poskušal spodbuditi k ravnanju, ki je v nasprotju z zakonom ali pogodbo, ali če je zaradi drugih resnih dogodkov nadaljnje sodelovanje v strukturi tega PARTNERJA nesprejemljivo. PROFIT PLANET odloča o ustrezni pisni vlogi po lastni presoji.</p>
<p>6.4 PARTNER-ja, ki je v zadnjih 12 mesecih že imel pogodbo s PROFIT PLANET, ni mogoče ponovno pridobiti kot novega PARTNER-ja.</p>
<p>6.5 Obhajanje okrilja PARTNER-ja, na primer z uporabo slamnatih imen, oseb ali podjetij, je prepovedano.</p>
<p>6.6 PROFIT PLANET izrecno ne podeljuje teritorialne zaščite. Vsi PARTNER-ji lahko delujejo brez omejitev po vsej Evropi.</p>
<h2>7. PROVIZIJA</h2>
<p>7.1 Za vsako uspešno posredovano pogodbo med ponudnikom in končnim kupcem PARTNER pridobi pravico do provizije kot nadomestilo za obdelavo in stroške.</p>
<p>7.2 Višina provizije je določena z veljavno preglednico provizij iz marketinškega koncepta. Veljavna različica te preglednice je vedno dostopna, vidna, prenosljiva na spletni strani PROFIT PLANET (<a href="http://www.profit-planet.com" target="_new">www.profit-planet.com</a>), in je na voljo tudi na zahtevo. Spremembe preglednice provizij bodo PARTNER-ju pravočasno sporočene. Veljajo provizijske postavke, ki so veljavne v času posredovanja.</p>
<p>7.3 Kot uspešno posredovanje v smislu te pogodbe velja, če je pogodba med končnim kupcem in ponudnikom dejansko sklenjena. Pravica do provizije odpade, če:</p>
<ul>
<li>kupec uveljavi pravico do odstopa ali preklica,</li>
<li>je pogodba pravno razveljavljena,</li>
<li>kupec iz katerega koli razloga ni sprejet s strani ponudnika izdelka ali storitve,</li>
<li>so vložene napačne ali nepopolne prijave kupcev,</li>
<li>je pogodba sklenjena nezakonito ali</li>
<li>ponudnik izdelka ali storitve zavrne izplačilo provizije PROFIT PLANET-u iz razlogov, za katere PROFIT PLANET ni odgovoren.</li>
</ul>
<p>7.4 Pravica do izplačila provizije obstaja do PROFIT PLANET-a načeloma šele takrat, ko je PROFIT PLANET prejel plačila s strani poslovnega partnerja/ponudnika in so izpolnjeni tudi vsi drugi pogoji za izplačilo. PARTNER se zaveda, da se natančne plačilne določbe med ponudniki razlikujejo, in PROFIT PLANET te razlike upošteva pri izplačilih. Trenutno se časovni razponi med partnerskimi podjetji povprečno gibljejo med 30 in 100 dnevi. Natančne zahteve in pogoji izhajajo iz informacijskega lista partnerja/ponudnika in iz marketinškega koncepta.</p>
<p>7.5 Izplačila s strani PROFIT PLANET-a se vršijo enkrat mesečno, približno 20. v mesecu, po mesecu, v katerem je PROFIT PLANET prejel plačila. Izplačilo se izvede brezgotovinsko z nakazilom na račun, ki ga je navedel PARTNER. PROFIT PLANET lahko izključi plačila do višine 100 EUR (minimalni znesek za izplačilo); neizplačane provizije se knjižijo na provizijski račun PARTNER in izplačajo v naslednjem mesecu, ko je dosežen minimalni znesek za izplačilo. Zneski pod minimalnim pragom se izplačajo enkrat letno.</p>
<p>7.6 Pravica do provizije retroaktivno preneha, če mora PROFIT PLANET vrniti provizije ponudniku, na primer ker kupec odstopi od pogodbe ali če obstajajo drugi razlogi za izključitev s strani ponudnika (npr. odgovornost za preklic oz. stornacijo). PROFIT PLANET ima pravico, da terjatve, do katerih je upravičen do PARTNERJA, v celoti ali delno pobota s terjatvami slednjega iz naslova provizij.</p>
<p>7.7 S to provizijo so poravnane vse aktivnosti PARTNER-ja, vključno z vsemi stroški, izdatki in stroški, ki jih ima v zvezi s to pogodbo, kot so potni stroški, stroški pisarne, poštnine, telefonski stroški ipd. Enako velja za aktivnosti PARTNER-ja v zvezi z vzdrževanjem in pridobivanjem baze PARTNER-jev ali strank, tako da ob prenehanju pogodbe, ne glede na razlog, ni zahtevkov do PROFIT PLANET-a iz naslova odpravnin ali odškodnin.</p>
<p>7.8 Nepravilna izplačila provizij ali druga izplačila mora PARTNER pisno zahtevati v 60 dneh. Po tem roku se izplačila štejejo kot sprejeta.</p>
<p>7.9 Če PARTNER ne navede ID za DDV, se vsa izplačila izvršijo neto.</p>
<h2>8. POGODBENE KAZNI, ODŠKODNINA</h2>
<p>8.1 Ob prvi kršitvi obveznosti iz te pogodbe s strani PARTNER-ja mu bo PROFIT PLANET podal pisno opozorilo. Kršitev oz. stanje, ki je nastalo, je treba takoj odpraviti.</p>
<p>8.2 Če pride do ponovne kršitve te pogodbe ali če se prvotno opozorjenega stanja ne odpravi, se PARTNER zavezuje plačati pogodbeno kazen ne glede na krivdo, in sicer v višini 5.000 EUR za vsako posamezno kršitev.</p>
<p>8.3 Ob kršitvah obveznosti glede zaupnosti in varstva podatkov ter ob posebej hudih kršitvah, zlasti proti točki 10.2 te pogodbe, je PROFIT PLANET upravičen uveljavljati pogodbeno kazen brez predhodnega opozorila.</p>
<p>8.4 Za vsako kršitev točke 3.11 te pogodbe (npr. posredovanje pogodb ali strank drugim podjetjem ali tretjim osebam) se PARTNER zavezuje plačati PROFIT PLANET-u pogodbeno kazen, ne glede na krivdo ali škodo, in sicer 5.000 EUR za vsako kršitev. Uveljavljanje dodatnih odškodninskih zahtevkov, pogodbene kazni po točki 8.2 ali zahtevkov za izpolnitev s tem ni izključeno.</p>
<p>8.5 Za vsako kršitev obveznosti iz točke 4. te pogodbe (obveznost varovanja zaupnosti) se PARTNER zavezuje plačati pogodbeno kazen, ne glede na krivdo ali škodo, v višini 7.000 EUR za vsako posamezno kršitev. Uveljavljanje nadaljnjih civilnopravnih zahtevkov zlasti prepovednih ali odškodninskih s tem ni izključeno.</p>
<p>8.6 Za dejanja, ki ustrezajo pogojem za izredno odpoved iz točke 10.2, zlasti za nepošteno ravnanje v smislu tam opisanih primerov (npr. nepooblaščeno kontaktiranje strank, škodljivo ravnanje, ki škodi ugledu, nepooblaščeno nastopanje v imenu PROFIT PLANET-a), se PARTNER zavezuje plačati pogodbeno kazen, ne glede na krivdo ali škodo, v višini 10.000 EUR za vsako kršitev. Tudi v teh primerih so izrecno pridržani nadaljnji zahtevki zlasti odškodninski ali izredna odpoved.</p>
<h2>9. IZKLJUČITEV ODGOVORNOSTI</h2>
<p>9.1 PARTNER opravlja svoje dejavnosti po svojih najboljših močeh in znanju ter na lastno odgovornost, zlasti tudi glede pravilnega svetovanja končnim strankam. PROFIT PLANET izrecno ne prevzema odgovornosti za napačno svetovanje ali drugo napačno ravnanje PARTNER-ja.</p>
<p>9.2 Za škodo PROFIT PLANET odgovarja samo, če je ta nastala zaradi namena ali hude malomarnosti ali hude kršitve bistvene pogodbene obveznosti s strani PROFIT PLANET-a, njegovih zaposlenih ali pomočnikov.</p>
<p>9.3 Odgovornost PROFIT PLANET-a za posredno škodo, posledično škodo, izgubljeni dobiček ali pričakovane prihranke je v vsakem primeru izključena.</p>
<p>9.4 PROFIT PLANET ne prevzema odgovornosti za škodo, nastalo zaradi izgube podatkov na strežnikih, razen če je škoda nastala zaradi namere ali hude malomarnosti s strani PROFIT PLANET-a, njegovih zaposlenih ali pomočnikov.</p>
<p>9.5 O nastanku škode mora PARTNER nemudoma obvestiti PROFIT PLANET.</p>
<h2>10. TRAJANJE IN PRENEHANJE POGODBE</h2>
<p>10.1 Pogodba začne veljati z dnem podpisa ali v primeru spletne registracije z odobritvijo pogodbe s strani PROFIT PLANET-a in je sklenjena za nedoločen čas. Obe pogodbeni stranki jo lahko odpovesta s trimesečnim odpovednim rokom ob koncu koledarskega meseca, v pisni obliki.</p>
<p>10.2 Ne glede na to lahko PROFIT PLANET pogodbo izredno odpove brez odpovednega roka iz pomembnega razloga. Pravica do izredne odpovedi obstaja ne glede na druge zahtevke. Med pomembne razloge zlasti sodijo, pri čemer seznam ni dokončen:</p>
<ul>
<li>dejanja nepoštenega ravnanja, ki onemogočajo nadaljnje sodelovanje med pogodbenimi strankami;</li>
<li>takšno nepošteno ravnanje obstaja zlasti, če PARTNER brez izrecnega soglasja zastopnika PROFIT PLANET-a sprejema ukrepe, ki navzven dajejo vtis, da deluje v imenu ali po naročilu PROFIT PLANET-a, npr. nepooblaščeno kontaktiranje strank, uporaba poslovnih dokumentov ali podpisov, nastopanje z uporabo blagovne znamke PROFIT PLANET ali podobne škodljive aktivnosti;</li>
<li>uporaba nepoštenih praks ali hujša ali ponavljajoča se kršitev te pogodbe ter kršitve obveznih zakonskih določb;</li>
<li>če je za premoženje druge pogodbene stranke vložen predlog za začetek insolvenčnega postopka ali če je začetek postopka zavrnjen zaradi pomanjkanja sredstev;</li>
<li>kršitev dogovorjenih ali zakonskih obveznosti varstva podatkov ali zaupnosti;</li>
<li>če bi sodelovanje zaradi ravnanja ene stranke ali njenega ugleda v javnosti povzročilo škodo ugledu druge stranke;</li>
<li>če sodelovanje zaradi zakonodaje ali zaradi tretjih oseb postane prepovedano;</li>
<li>nedovoljeni dogovori s tretjimi osebami, vključenimi v prodajo.</li>
</ul>
<p>10.3 Poleg točke 10.2 lahko PROFIT PLANET PARTNERJU tudi izredno odpove pogodbo, če ta v zadnjih šestih mesecih ni ustvaril novih prihodkov ali če so bili v pogodbah, ki jih je posredoval med končnimi strankami in ponudniki, v obdobju dveh mesecev nadpovprečni deleži preklicev (nad 30 % posredovanih pogodb). PROFIT PLANET bo PARTNER-ju pred izredno odpovedjo po tej točki enkrat pisno opozoril, da ima možnost v 30 dneh ustvariti zahtevane prihodke ali izboljšati delež preklicev.</p>
<p>10.4 Po prenehanju pogodbe PARTNER nima več pravice do provizije, razen za že uspešno posredovane pogodbe. Pravica do nadomestila za komercialnega zastopnika je izrecno izključena, saj PARTNER ne deluje kot komercialni zastopnik PROFIT PLANET-a. Morebitne zahteve po nadaljnjih provizijah za posredovane izdelke obstajajo še 12 mesecev po prenehanju pogodbe; v primeru izredne odpovedi pogodbe te pravice takoj prenehajo.</p>
<p>10.5 Po prenehanju pogodbe mora PARTNER brez poziva v enem mesecu vrniti vse dodeljene dokumente in oglaševalske materiale PROFIT PLANET-u. Uporaba blagovne znamke PROFIT PLANET in ustreznih logotipov - npr. na dopisih ali v podpisih elektronske pošte - po prenehanju pogodbe ni dovoljena.</p>
<h2>11. PRENOS</h2>
<p>11.1 PROFIT PLANET je kadar koli upravičen, da celotno dejavnost ali njen del prenese na tretje osebe.</p>
<p>11.2 PARTNER je lahko svojo prodajno strukturo prenese na tretjo osebo le s predhodnim izrecnim soglasjem PROFIT PLANET-a.</p>
<p>11.3 Če kapitalska ali osebna družba, registrirana kot PARTNER, sprejme novega družbenika, to ne vpliva na to pogodbo, če družbenik/i, ki so prvotno podpisali vlogo za partnerja, ostanejo v družbi kot družbeniki. Če družbenik iz registrirane družbe izstopi ali prenese svoj delež na tretjo osebo, je to dovoljeno, če PROFIT PLANET o tem pisno obvesti skupaj z ustreznimi pravno veljavnimi dokumenti in če ta postopek ni v nasprotju z drugimi določili te pogodbe; v nasprotnem primeru si PROFIT PLANET pridržuje pravico odpovedati pogodbo s to kapitalsko ali osebno družbo.</p>
<p>11.4 Ob razpadu skupnosti, registrirane kot PARTNERSTVO (kapitalska ali osebna družba, zakonska skupnost ipd., ki imajo skupno partnersko pogodbo), ostane v veljavi le ena partnerska pogodba. Člani razpadajoče skupnosti se morajo interno dogovoriti, kateri član/družbenik bo nadomestil prodajnega/poslovnega/povezanega partnerja, in morajo o tem pisno obvestiti PROFIT PLANET. Če člani skupnosti ne morejo doseči veljavnega pogodbenega dogovora glede nadaljevanja partnerske pogodbe, si PROFIT PLANET pridržuje pravico do izredne odpovedi, zlasti če nesoglasje glede posledic povzroči zanemarjanje obveznosti sodelovanja, kršitev te pogodbe ali veljavne zakonodaje ali preveliko obremenitev prodajne strukture partnerskega sodelovanja.</p>
<h2>12. KONČNE DOLOČBE</h2>
<p>12.1 Spremembe in dopolnitve te pogodbe morajo biti v pisni obliki. To velja tudi za opustitev zahteve po pisni obliki. Ustni dodatni dogovori ne obstajajo.</p>
<p>12.2 Če je kakšna določba te pogodbe neveljavna ali postane neveljavna, velja namesto neveljavne določbe tista določba, ki najbolj ustreza gospodarskemu namenu neveljavne določbe.</p>
<p>12.3 Dogovorjena pristojnost za vse spore, ki izhajajo iz te pogodbe ali so z njo povezani, je stvarno pristojno sodišče v Gradcu, Avstrija. Ta pogodba je predmet avstrijskega prava, pri čemer se izključijo neobvezna kolizijska pravila IPR. Nadaljnje ali povratne sklicevanje je izključeno. Poleg tega lahko PROFIT PLANET vloži tožbo proti PARTNER-ju na sodišču v kraju njegove splošne pristojnosti.</p>
<div class="signatures">
<div class="signature" style="flex:1;text-align:center;">
<p style="margin:0 0 6px;">Za PROFIT PLANET</p>
<div class="sig-block" style="display:flex;flex-direction:column;align-items:center;gap:6px;min-height:140px;">
<div class="pp-stamp" style="display:block;max-width:220px;margin:0 auto 6px;">{{profitplanetSignature}}</div>
<div style="font-size:0.75em;line-height:1.2;">{{currentDate}}</div>
</div>
<p style="margin:0 0 6px;">Datum, podpis</p>
</div>
<div class="signature">
<p style="margin:0 0 6px;">Za PARTNERJA</p>
<div class="sig-block">
<span>{{signatureImage}}</span>
<div style="font-size:0.75em;line-height:1.2;">{{fullName}}</div>
<div style="font-size:0.75em;line-height:1.2;">{{currentDate}}</div>
</div>
<p style="margin:0 0 6px;">Ime in priimek, datum, podpis</p>
</div>
</div>
</div>
</body>
</html>