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

Reviewed-on: #15
This commit is contained in:
Seazn 2026-03-16 19:54:12 +00:00
commit e4e285eeff
55 changed files with 4026 additions and 2343 deletions

BIN
QRCode/qr_120.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
QRCode/qr_60.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -25,7 +25,6 @@ module.exports = {
isForSelf: req.body.is_for_self, isForSelf: req.body.is_for_self,
recipientName: req.body.recipient_name, recipientName: req.body.recipient_name,
recipientEmail: req.body.recipient_email, recipientEmail: req.body.recipient_email,
recipientNotes: req.body.recipient_notes,
firstName: req.body.firstName, firstName: req.body.firstName,
lastName: req.body.lastName, lastName: req.body.lastName,
email: req.body.email, email: req.body.email,
@ -34,7 +33,21 @@ module.exports = {
city: req.body.city, city: req.body.city,
country: req.body.country, country: req.body.country,
frequency: req.body.frequency, frequency: req.body.frequency,
startDate: req.body.startDate, phone: req.body.phone,
recipientContractName: req.body.recipientContractName,
recipientAddress: req.body.recipientAddress,
paymentMethod: req.body.paymentMethod,
invoiceByEmail: req.body.invoiceByEmail,
invoiceSameAsShipping: req.body.invoiceSameAsShipping,
invoiceFullName: req.body.invoiceFullName,
invoiceStreet: req.body.invoiceStreet,
invoicePostalCode: req.body.invoicePostalCode,
invoiceCity: req.body.invoiceCity,
invoicePhone: req.body.invoicePhone,
invoiceEmail: req.body.invoiceEmail,
contractNumber: req.body.contractNumber || req.body.contract_number,
signingCity: req.body.signingCity || req.body.signing_city || '',
signatureDataUrl: req.body.signatureDataUrl || req.body.signature_data_url,
actorUser, // normalized to include id actorUser, // normalized to include id
referredBy: req.body.referred_by, referredBy: req.body.referred_by,
}); });

View File

@ -1,12 +1,122 @@
const CompanySettingsRepository = require('../../repositories/settings/CompanySettingsRepository'); const CompanySettingsRepository = require('../../repositories/settings/CompanySettingsRepository');
const { logger } = require('../../middleware/logger');
const crypto = require('crypto');
const repo = new CompanySettingsRepository(); const repo = new CompanySettingsRepository();
class CompanySettingsController { 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;
}
static async get(req, res) { static async get(req, res) {
try { try {
const settings = await repo.get(); const settings = await repo.get();
return res.json(settings || { company_name: '', company_street: '', company_postal_city: '', company_country: '' }); return res.json(settings || {
company_name: '',
company_street: '',
company_postal_city: '',
company_country: '',
qr_code_60_base64: null,
qr_code_120_base64: null,
});
} catch (err) { } catch (err) {
return res.status(500).json({ message: 'Failed to load company settings' }); return res.status(500).json({ message: 'Failed to load company settings' });
} }
@ -14,10 +124,141 @@ class CompanySettingsController {
static async update(req, res) { static async update(req, res) {
try { try {
const { company_name, company_street, company_postal_city, company_country } = req.body; const body = req.body || {};
const updated = await repo.update({ company_name, company_street, company_postal_city, company_country });
const requestId = req.id;
const contentType = (req.get('content-type') || '').toLowerCase();
const contentLength = req.get('content-length') || null;
if (contentType.includes('multipart/form-data')) {
logger.warn('companySettings:update:multipart_not_supported', {
requestId,
contentType: req.get('content-type') || null,
hint: 'Send JSON (application/json) or add multer middleware for this route.'
});
}
if (Object.keys(body).length === 0 && contentLength && Number(contentLength) > 0) {
logger.warn('companySettings:update:empty_parsed_body', {
requestId,
contentType: req.get('content-type') || null,
contentLength,
hint: 'Body parser may not match the request content-type or size limit.'
});
}
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),
};
// Only forward keys that were actually provided (so we don't wipe values on partial updates)
const provided = {};
for (const [key, value] of Object.entries(payload)) {
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); return res.json(updated);
} catch (err) { } catch (err) {
logger.error('companySettings:update:failed', {
requestId: req && req.id,
message: err?.message,
stack: err?.stack,
});
return res.status(500).json({ message: 'Failed to update company settings' }); return res.status(500).json({ message: 'Failed to update company settings' });
} }
} }

View File

@ -0,0 +1,120 @@
const { v4: uuidv4 } = require('uuid');
const DashboardPlatformsRepository = require('../../repositories/settings/DashboardPlatformsRepository');
const repo = new DashboardPlatformsRepository();
function toBool(value, fallback) {
if (value === undefined) return fallback;
if (value === true || value === false) return value;
if (value === 'true') return true;
if (value === 'false') return false;
return fallback;
}
function toNullableString(value) {
if (value === undefined) return undefined;
if (value === null) return null;
const s = String(value);
return s;
}
function requireNonEmptyString(value, field) {
const s = typeof value === 'string' ? value.trim() : '';
if (!s) {
const err = new Error(`${field} is required`);
err.status = 400;
throw err;
}
return s;
}
class DashboardPlatformsController {
// Public endpoint (no auth): only returns active platforms (state=true)
static async listPublic(req, res) {
const list = await repo.listPublic();
return res.json(list);
}
static async list(req, res) {
const list = await repo.list();
return res.json(list);
}
static async create(req, res) {
try {
const title = requireNonEmptyString(req.body.title, 'title');
const href = requireNonEmptyString(req.body.href, 'href');
const platform = {
id: uuidv4(),
title,
description: toNullableString(req.body.description) ?? '',
href,
icon: toNullableString(req.body.icon) ?? '',
color: toNullableString(req.body.color) ?? '',
state: toBool(req.body.state, true),
disabled: toBool(req.body.disabled, false),
disabledText: toNullableString(req.body.disabledText) ?? null,
sortOrder: Number.isFinite(Number(req.body.sortOrder)) ? Number(req.body.sortOrder) : 0,
};
const created = await repo.create(platform);
return res.status(201).json(created || platform);
} catch (err) {
const status = err?.status || 500;
return res.status(status).json({ error: err.message || 'Failed to create dashboard platform' });
}
}
static async update(req, res) {
try {
const id = String(req.params.id || '').trim();
if (!id) return res.status(400).json({ error: 'id is required' });
const existing = await repo.getById(id);
if (!existing) return res.status(404).json({ error: 'Not found' });
const next = {
...existing,
title: req.body.title === undefined ? existing.title : String(req.body.title),
description: req.body.description === undefined ? existing.description : String(req.body.description || ''),
href: req.body.href === undefined ? existing.href : String(req.body.href),
icon: req.body.icon === undefined ? existing.icon : String(req.body.icon || ''),
color: req.body.color === undefined ? existing.color : String(req.body.color || ''),
disabled: req.body.disabled === undefined ? existing.disabled : toBool(req.body.disabled, existing.disabled),
disabledText: req.body.disabledText === undefined ? existing.disabledText : (req.body.disabledText === null ? null : String(req.body.disabledText)),
sortOrder: req.body.sortOrder === undefined ? existing.sortOrder : (Number.isFinite(Number(req.body.sortOrder)) ? Number(req.body.sortOrder) : existing.sortOrder),
};
// If title/href were provided, require non-empty
if (req.body.title !== undefined) requireNonEmptyString(next.title, 'title');
if (req.body.href !== undefined) requireNonEmptyString(next.href, 'href');
const updated = await repo.update(id, next);
if (!updated) return res.status(404).json({ error: 'Not found' });
return res.json(updated);
} catch (err) {
const status = err?.status || 500;
return res.status(status).json({ error: err.message || 'Failed to update dashboard platform' });
}
}
static async setState(req, res) {
try {
const id = String(req.params.id || '').trim();
if (!id) return res.status(400).json({ error: 'id is required' });
const state = toBool(req.body.state, undefined);
if (state === undefined) return res.status(400).json({ error: 'state must be boolean' });
const updated = await repo.setState(id, state);
if (!updated) return res.status(404).json({ error: 'Not found' });
return res.json(updated);
} catch (err) {
const status = err?.status || 500;
return res.status(status).json({ error: err.message || 'Failed to update state' });
}
}
}
module.exports = DashboardPlatformsController;

View File

@ -0,0 +1,25 @@
const CoffeeShippingFeeService = require('../../services/subscriptions/CoffeeShippingFeeService');
class ShippingFeesController {
// Public endpoint: returns the 2 configured shipping fees (60/120)
static async listPublic(req, res) {
const list = await CoffeeShippingFeeService.list();
return res.json(list);
}
// Admin endpoint: update price for a pieceCount (60 or 120)
static async updatePrice(req, res) {
try {
const pieceCount = req.params.pieceCount;
const price = req.body?.price;
const updated = await CoffeeShippingFeeService.setPrice(pieceCount, price);
if (!updated) return res.status(404).json({ error: 'Not found' });
return res.json(updated);
} catch (err) {
const status = err?.status || 500;
return res.status(status).json({ error: err?.message || 'Failed to update shipping fee' });
}
}
}
module.exports = ShippingFeesController;

View File

@ -22,7 +22,7 @@ class EmailVerificationController {
res.json({ success: true, message: 'Verification email sent' }); res.json({ success: true, message: 'Verification email sent' });
} catch (error) { } catch (error) {
await unitOfWork.rollback(error); await unitOfWork.rollback(error);
logger.error('Error sending verification email:', error); logger.error('Error sending verification email', { message: error?.message, stack: error?.stack });
res.status(400).json({ success: false, message: error.message }); res.status(400).json({ success: false, message: error.message });
} }
} }
@ -44,7 +44,7 @@ class EmailVerificationController {
} }
} catch (error) { } catch (error) {
await unitOfWork.rollback(error); await unitOfWork.rollback(error);
logger.error('Error verifying email code:', error); logger.error('Error verifying email code', { message: error?.message, stack: error?.stack });
res.status(400).json({ success: false, error: error.message }); res.status(400).json({ success: false, error: error.message });
} }
} }

View File

@ -332,6 +332,95 @@ function ensureHtmlDocument(html) {
`; `;
} }
function escapeRegExp(str) {
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Raw preview helper: fetch latest active contract template HTML (placeholders intact)
async function fetchLatestActiveContractTemplateHtml({ userType, contractType }) {
const allowedContractTypes = ['contract', 'gdpr', 'abo'];
const safeContractType = allowedContractTypes.includes(String(contractType || '').toLowerCase())
? String(contractType).toLowerCase()
: 'contract';
const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, 'contract', safeContractType);
if (!latest) return { latest: null, html: '' };
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
}
});
const command = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: latest.storageKey });
const fileObj = await s3.send(command);
if (!fileObj.Body) {
logger.warn('[fetchLatestActiveContractTemplateHtml] empty S3 body', { id: latest.id, key: latest.storageKey });
return { latest, html: '' };
}
const html = await streamToString(fileObj.Body, latest.id);
return { latest, html: ensureHtmlDocument(html) };
}
async function renderLatestActiveContractHtmlForUser({ targetUserId, userType, contractType, req = null }) {
const allowedContractTypes = ['contract', 'gdpr', 'abo'];
const safeContractType = allowedContractTypes.includes(String(contractType || '').toLowerCase())
? String(contractType).toLowerCase()
: 'contract';
const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, 'contract', safeContractType);
if (!latest) return { latest: null, html: '' };
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
}
});
const command = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: latest.storageKey });
const fileObj = await s3.send(command);
if (!fileObj.Body) {
const err = new Error('Template file not available');
err.status = 404;
throw err;
}
let html = await streamToString(fileObj.Body, latest.id);
const uow = new UnitOfWork();
let vars;
try {
await uow.start();
vars = await ContractUploadService.buildTemplateVars({
userId: targetUserId,
user_type: userType,
contractData: {},
unitOfWork: uow
});
await uow.commit();
} catch (e) {
try { await uow.rollback(e); } catch (_) {}
throw e;
}
Object.entries(vars).forEach(([k, v]) => {
const pattern = new RegExp(`{{\\s*${escapeRegExp(k)}\\s*}}`, 'g');
html = html.replace(pattern, String(v ?? ''));
});
html = html.replace(/{{\s*signatureImage\s*}}/g, 'Your signature will appear here');
html = sanitizePlaceholders(html, ['companyStamp','companyStampInline','companyStampSmall','profitplanetSignature']);
const reqForStamp = (req && req.user) ? req : { user: { id: targetUserId, user_type: userType } };
try { html = await applyCompanyStampPlaceholders(html, reqForStamp); } catch (e) {}
try { html = await applyProfitPlanetSignature(html); } catch (e) {}
return { latest, html: ensureHtmlDocument(html) };
}
exports.listTemplates = async (req, res) => { exports.listTemplates = async (req, res) => {
const templates = await DocumentTemplateService.listTemplates(); const templates = await DocumentTemplateService.listTemplates();
const s3 = new S3Client({ const s3 = new S3Client({
@ -385,9 +474,12 @@ exports.uploadTemplate = async (req, res) => {
if (!file) return res.status(400).json({ error: 'No file uploaded' }); if (!file) return res.status(400).json({ error: 'No file uploaded' });
if (!lang || !['en', 'de'].includes(lang)) return res.status(400).json({ error: 'Invalid or missing language' }); if (!lang || !['en', 'de'].includes(lang)) return res.status(400).json({ error: 'Invalid or missing language' });
const allowedContractTypes = ['contract', 'gdpr']; const allowedContractTypes = ['contract', 'gdpr', 'abo'];
const contract_type = (type === 'contract' && allowedContractTypes.includes(rawContractType)) const normalizedContractType = rawContractType !== undefined && rawContractType !== null
? rawContractType ? String(rawContractType).trim().toLowerCase()
: rawContractType;
const contract_type = (type === 'contract' && allowedContractTypes.includes(normalizedContractType))
? normalizedContractType
: (type === 'contract' ? 'contract' : null); : (type === 'contract' ? 'contract' : null);
// Use "english" for en, "german" for de // Use "english" for en, "german" for de
@ -449,12 +541,13 @@ exports.updateTemplate = async (req, res) => {
if (!current) return res.status(404).json({ error: 'Template not found' }); if (!current) return res.status(404).json({ error: 'Template not found' });
const nextType = type !== undefined ? type : current.type; const nextType = type !== undefined ? type : current.type;
const allowedContractTypes = ['contract', 'gdpr']; const allowedContractTypes = ['contract', 'gdpr', 'abo'];
let contract_type = null; let contract_type = null;
if (nextType === 'contract') { if (nextType === 'contract') {
const candidate = rawContractType !== undefined ? rawContractType : current.contract_type; const candidate = rawContractType !== undefined ? rawContractType : current.contract_type;
if (candidate && allowedContractTypes.includes(candidate)) { const normalizedCandidate = candidate !== undefined && candidate !== null ? String(candidate).trim().toLowerCase() : candidate;
contract_type = candidate; if (normalizedCandidate && allowedContractTypes.includes(normalizedCandidate)) {
contract_type = normalizedCandidate;
} else { } else {
contract_type = 'contract'; contract_type = 'contract';
} }
@ -528,11 +621,12 @@ exports.reviseTemplate = async (req, res) => {
? rawUserType ? rawUserType
: (previous.user_type || 'both'); : (previous.user_type || 'both');
const allowedContractTypes = ['contract', 'gdpr']; const allowedContractTypes = ['contract', 'gdpr', 'abo'];
let contract_type = null; let contract_type = null;
if (nextType === 'contract') { if (nextType === 'contract') {
const candidate = rawContractType !== undefined ? rawContractType : previous.contract_type; const candidate = rawContractType !== undefined ? rawContractType : previous.contract_type;
contract_type = allowedContractTypes.includes(candidate) ? candidate : 'contract'; const normalizedCandidate = candidate !== undefined && candidate !== null ? String(candidate).trim().toLowerCase() : candidate;
contract_type = allowedContractTypes.includes(normalizedCandidate) ? normalizedCandidate : 'contract';
} }
// Use "english" for en, "german" for de // Use "english" for en, "german" for de
@ -1180,9 +1274,10 @@ exports.generatePdfWithSignature = async (req, res) => {
// Insert userData into HTML (simple replace, adjust as needed) // Insert userData into HTML (simple replace, adjust as needed)
if (userData && typeof userData === 'object') { if (userData && typeof userData === 'object') {
Object.entries(userData).forEach(([key, value]) => { Object.entries(userData).forEach(([key, value]) => {
const beforeCount = (html.match(new RegExp(`{{\s*${key}\s*}}`, 'g')) || []).length; const pattern = new RegExp(`{{\\s*${escapeRegExp(key)}\\s*}}`, 'g');
html = html.replace(new RegExp(`{{\s*${key}\s*}}`, 'g'), value); const beforeCount = (html.match(pattern) || []).length;
const afterCount = (html.match(new RegExp(`{{\s*${key}\s*}}`, 'g')) || []).length; html = html.replace(pattern, String(value ?? ''));
const afterCount = (html.match(pattern) || []).length;
if (beforeCount || afterCount) { if (beforeCount || afterCount) {
logger.debug(`[generatePdfWithSignature] replaced ${key}: before=${beforeCount} after=${afterCount}`); logger.debug(`[generatePdfWithSignature] replaced ${key}: before=${beforeCount} after=${afterCount}`);
} }
@ -1458,11 +1553,11 @@ exports.previewPdf = async (req, res) => {
}; };
// NEW: Admin-only endpoint to preview the user's uploaded/signed contract (contract or gdpr) from object storage only // NEW: Admin-only endpoint to preview the user's uploaded/signed contract (contract or gdpr) from object storage only
// GET /api/admin/contracts/:id/preview?userType=personal|company&contract_type=contract|gdpr // GET /api/admin/contracts/:id/preview?userType=personal|company&contract_type=contract|gdpr|abo
exports.previewLatestForUser = async (req, res) => { exports.previewLatestForUser = async (req, res) => {
const targetUserId = parseInt(req.params.id, 10); const targetUserId = parseInt(req.params.id, 10);
const contractTypeParam = (req.query.contract_type || req.query.contractType || '').toString().toLowerCase(); const contractTypeParam = (req.query.contract_type || req.query.contractType || '').toString().toLowerCase();
const allowedContractTypes = ['contract', 'gdpr']; const allowedContractTypes = ['contract', 'gdpr', 'abo'];
const contractType = allowedContractTypes.includes(contractTypeParam) ? contractTypeParam : 'contract'; const contractType = allowedContractTypes.includes(contractTypeParam) ? contractTypeParam : 'contract';
const documentId = parseInt((req.query.documentId || req.query.document_id || '').toString(), 10); const documentId = parseInt((req.query.documentId || req.query.document_id || '').toString(), 10);
@ -1518,7 +1613,8 @@ exports.previewLatestForUser = async (req, res) => {
if (keys.length > 0) { if (keys.length > 0) {
const hasContractFolder = keys.some(k => k.startsWith(`${basePrefix}contract/`)); const hasContractFolder = keys.some(k => k.startsWith(`${basePrefix}contract/`));
const hasGdprFolder = keys.some(k => k.startsWith(`${basePrefix}gdpr/`)); const hasGdprFolder = keys.some(k => k.startsWith(`${basePrefix}gdpr/`));
if (!hasContractFolder && !hasGdprFolder) { const hasAboFolder = keys.some(k => k.startsWith(`${basePrefix}abo/`));
if (!hasContractFolder && !hasGdprFolder && !hasAboFolder) {
folderStructureWarning = 'Admin user has to clean up and move the files in exoscale folder'; folderStructureWarning = 'Admin user has to clean up and move the files in exoscale folder';
logger.error('[previewLatestForUser] contract folder structure invalid: Admin user has to clean up and move the files in exoscale folder', { logger.error('[previewLatestForUser] contract folder structure invalid: Admin user has to clean up and move the files in exoscale folder', {
userId: targetUserId, userId: targetUserId,
@ -1568,6 +1664,7 @@ exports.previewLatestForUser = async (req, res) => {
} }
if (key.includes('/gdpr/')) resolvedContractType = 'gdpr'; if (key.includes('/gdpr/')) resolvedContractType = 'gdpr';
else if (key.includes('/contract/')) resolvedContractType = 'contract'; else if (key.includes('/contract/')) resolvedContractType = 'contract';
else if (key.includes('/abo/')) resolvedContractType = 'abo';
doc = { object_storage_id: key }; doc = { object_storage_id: key };
logger.info('[previewLatestForUser] using objectKey directly', { targetUserId, resolvedContractType, key }); logger.info('[previewLatestForUser] using objectKey directly', { targetUserId, resolvedContractType, key });
} else { } else {
@ -1576,7 +1673,8 @@ exports.previewLatestForUser = async (req, res) => {
// so use contract_type column to disambiguate between contract vs gdpr // so use contract_type column to disambiguate between contract vs gdpr
const docTypesMap = { const docTypesMap = {
contract: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf'], contract: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf'],
gdpr: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf'] gdpr: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf'],
abo: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf']
}; };
const docTypes = docTypesMap[contractType] || docTypesMap.contract; const docTypes = docTypesMap[contractType] || docTypesMap.contract;
const placeholders = docTypes.map(() => '?').join(','); const placeholders = docTypes.map(() => '?').join(',');
@ -1788,6 +1886,7 @@ exports.listUserContractDocuments = async (req, res) => {
let folderType = 'loose'; let folderType = 'loose';
if (/\/gdpr\//i.test(key)) folderType = 'gdpr'; if (/\/gdpr\//i.test(key)) folderType = 'gdpr';
else if (/\/contract\//i.test(key)) folderType = 'contract'; else if (/\/contract\//i.test(key)) folderType = 'contract';
else if (/\/abo\//i.test(key)) folderType = 'abo';
return { return {
...d, ...d,
folderType folderType
@ -1809,7 +1908,7 @@ exports.listUserContractDocuments = async (req, res) => {
exports.moveUserContractDocument = async (req, res) => { exports.moveUserContractDocument = async (req, res) => {
const targetUserId = parseInt(req.params.id, 10); const targetUserId = parseInt(req.params.id, 10);
const { documentId, targetType, objectKey } = req.body || {}; const { documentId, targetType, objectKey } = req.body || {};
const allowedTypes = ['contract', 'gdpr']; const allowedTypes = ['contract', 'gdpr', 'abo'];
const target = (targetType || '').toString().toLowerCase(); const target = (targetType || '').toString().toLowerCase();
if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) { if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) {
@ -1835,7 +1934,7 @@ exports.moveUserContractDocument = async (req, res) => {
if (objectKey) { if (objectKey) {
const key = String(objectKey); const key = String(objectKey);
const basePrefix = `contracts/${contractCategory}/${targetUserId}/`; const basePrefix = `contracts/${contractCategory}/${targetUserId}/`;
if (!key.startsWith(basePrefix) || (!key.includes('/contract/') && !key.includes('/gdpr/'))) { if (!key.startsWith(basePrefix) || (!key.includes('/contract/') && !key.includes('/gdpr/') && !key.includes('/abo/'))) {
return res.status(400).json({ error: 'Invalid object key' }); return res.status(400).json({ error: 'Invalid object key' });
} }
currentKey = key; currentKey = key;
@ -1906,7 +2005,7 @@ exports.moveUserContractDocument = async (req, res) => {
} }
}; };
// NEW: Authenticated user endpoint to preview their own latest active contract with DB-filled placeholders // Authenticated user endpoint to preview latest active contract template (raw HTML; placeholders intact)
// GET /api/contracts/preview/latest // GET /api/contracts/preview/latest
exports.previewLatestForMe = async (req, res) => { exports.previewLatestForMe = async (req, res) => {
if (!req.user) return res.status(401).json({ error: 'Unauthorized' }); if (!req.user) return res.status(401).json({ error: 'Unauthorized' });
@ -1915,72 +2014,48 @@ exports.previewLatestForMe = async (req, res) => {
if (!targetUserId || !userType) return res.status(400).json({ error: 'Invalid authenticated user' }); if (!targetUserId || !userType) return res.status(400).json({ error: 'Invalid authenticated user' });
const contractTypeParam = (req.query.contract_type || req.query.contractType || '').toString().toLowerCase(); const contractTypeParam = (req.query.contract_type || req.query.contractType || '').toString().toLowerCase();
const allowedContractTypes = ['contract', 'gdpr']; const allowedContractTypes = ['contract', 'gdpr', 'abo'];
const contractType = allowedContractTypes.includes(contractTypeParam) ? contractTypeParam : 'contract'; const contractType = allowedContractTypes.includes(contractTypeParam) ? contractTypeParam : 'contract';
try { try {
// Find the latest active template for this user type const { latest, html } = await fetchLatestActiveContractTemplateHtml({ userType, contractType });
const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, 'contract', contractType);
if (!latest) { if (!latest) {
logger.info('[previewLatestForMe] no active template', { userId: targetUserId, userType, contractType }); logger.info('[previewLatestForMe] no active template', { userId: targetUserId, userType, contractType });
// Return 200 with empty body so clients can show a friendly empty state. res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(200).send(''); return res.status(200).send('');
} }
// Fetch template HTML from storage
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
}
});
const command = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: latest.storageKey });
const fileObj = await s3.send(command);
if (!fileObj.Body) return res.status(404).json({ error: 'Template file not available' });
let html = await streamToString(fileObj.Body, latest.id);
// Build variables using the same logic as contract upload
const uow = new UnitOfWork();
await uow.start();
const vars = await ContractUploadService.buildTemplateVars({
userId: targetUserId,
user_type: userType,
contractData: {},
unitOfWork: uow
});
await uow.commit();
// Replace placeholders
Object.entries(vars).forEach(([k, v]) => {
html = html.replace(new RegExp(`{{\s*${k}\s*}}`, 'g'), String(v ?? ''));
});
// Show a friendly placeholder for signature in preview (not signed yet)
html = html.replace(/{{\s*signatureImage\s*}}/g, 'Your signature will appear here');
// Remove any remaining placeholders except stamp/signature markers
html = sanitizePlaceholders(html, ['companyStamp','companyStampInline','companyStampSmall','profitplanetSignature']);
// Log any remaining placeholders to aid debugging
const remaining = html.match(/{{\s*([^}\s]+)\s*}}/g) || [];
if (remaining.length) {
logger.debug('[previewLatestForMe] unreplaced placeholders', { count: remaining.length, samples: remaining.slice(0, 5) });
}
// Apply company stamp and signature where applicable
try { html = await applyCompanyStampPlaceholders(html, req); } catch (e) {}
try { html = await applyProfitPlanetSignature(html); } catch (e) {}
res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.send(ensureHtmlDocument(html)); return res.send(html);
} catch (err) { } catch (err) {
logger.error('[previewLatestForMe] error', err && err.stack ? err.stack : err); logger.error('[previewLatestForMe] error', err && err.stack ? err.stack : err);
return res.status(500).json({ error: 'Failed to render preview' }); return res.status(500).json({ error: 'Failed to render preview' });
} }
}; };
// User: convenience endpoint to preview latest active ABO contract (HTML) for authenticated user
// GET /api/contracts/abo/active
exports.previewLatestAboForMe = async (req, res) => {
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
const targetUserId = req.user.id || req.user.userId;
const userType = (req.user.user_type || req.user.userType || '').toString().toLowerCase();
if (!targetUserId || !userType) return res.status(400).json({ success: false, message: 'Invalid authenticated user' });
try {
const { latest, html } = await fetchLatestActiveContractTemplateHtml({ userType, contractType: 'abo' });
if (!latest) {
logger.info('[previewLatestAboForMe] no active template', { userId: targetUserId, userType, contractType: 'abo' });
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(200).send('');
}
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(200).send(html || '');
} catch (err) {
logger.error('[previewLatestAboForMe] error', err && err.stack ? err.stack : err);
const status = err && err.status ? err.status : 500;
return res.status(status).json({ success: false, message: 'Failed to render abo contract' });
}
};
// NEW controller: download sanitized PDF (variables emptied) // NEW controller: download sanitized PDF (variables emptied)
// Keeps stamp/signature placeholders so company stamps or profitplanetSignature can still be applied if available. // Keeps stamp/signature placeholders so company stamps or profitplanetSignature can still be applied if available.
exports.downloadPdf = async (req, res) => { exports.downloadPdf = async (req, res) => {

View File

@ -125,7 +125,10 @@ class LoginController {
lang: lang // <-- pass lang from request body lang: lang // <-- pass lang from request body
}); });
} catch (mailError) { } catch (mailError) {
logger.error('Error sending login notification email', { error: mailError }); logger.error('Error sending login notification email', {
message: mailError?.message,
stack: mailError?.stack,
});
// Do not block login // Do not block login
} }

View File

@ -72,7 +72,7 @@ module.exports = {
await uow.commit(); await uow.commit();
} catch (err) { } catch (err) {
await uow.rollback(); await uow.rollback();
logger.error('passwordReset:request_error', { error: err }); logger.error('passwordReset:request_error', { message: err?.message, stack: err?.stack });
} }
return res.json({ return res.json({
success: true, success: true,

View File

@ -167,7 +167,7 @@ const createDatabase = async () => {
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
user_type ENUM('personal', 'company') NOT NULL, user_type ENUM('personal', 'company', 'guest') NOT NULL,
role ENUM('user', 'admin', 'super_admin', 'guest') DEFAULT 'user', role ENUM('user', 'admin', 'super_admin', 'guest') DEFAULT 'user',
iban VARCHAR(34), iban VARCHAR(34),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -261,6 +261,9 @@ const createDatabase = async () => {
console.log(' Phone columns already nullable or ALTER not required'); console.log(' Phone columns already nullable or ALTER not required');
} }
// ATU number for company profiles
await addColumnIfMissing(connection, 'company_profiles', 'atu_number', `VARCHAR(50) NULL AFTER registration_number`);
// 4. user_status table: Comprehensive tracking of user verification and completion steps // 4. user_status table: Comprehensive tracking of user verification and completion steps
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS user_status ( CREATE TABLE IF NOT EXISTS user_status (
@ -375,7 +378,7 @@ const createDatabase = async () => {
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL, user_id INT NOT NULL,
document_type ENUM('personal_id', 'company_id', 'signature', 'contract', 'other') NOT NULL, document_type ENUM('personal_id', 'company_id', 'signature', 'contract', 'other') NOT NULL,
contract_type ENUM('contract','gdpr') NOT NULL DEFAULT 'contract', contract_type ENUM('contract','gdpr','abo') NOT NULL DEFAULT 'contract',
object_storage_id VARCHAR(255) UNIQUE NULL, object_storage_id VARCHAR(255) UNIQUE NULL,
signatureBase64 LONGTEXT NULL, signatureBase64 LONGTEXT NULL,
original_filename VARCHAR(255), original_filename VARCHAR(255),
@ -396,10 +399,21 @@ const createDatabase = async () => {
connection, connection,
'user_documents', 'user_documents',
'contract_type', 'contract_type',
`ENUM('contract','gdpr') NOT NULL DEFAULT 'contract' AFTER document_type` `ENUM('contract','gdpr','abo') NOT NULL DEFAULT 'contract' AFTER document_type`
); );
await ensureIndex(connection, 'user_documents', 'idx_user_contract_type', '`user_id`, `contract_type`'); await ensureIndex(connection, 'user_documents', 'idx_user_contract_type', '`user_id`, `contract_type`');
// Ensure enum includes 'abo' on existing schemas
try {
await connection.query(`
ALTER TABLE user_documents
MODIFY COLUMN contract_type ENUM('contract','gdpr','abo') NOT NULL DEFAULT 'contract';
`);
console.log("🔧 Ensured user_documents.contract_type includes 'abo'");
} catch (e) {
console.log(" user_documents.contract_type ENUM ALTER skipped:", e.message);
}
await connection.query(` await connection.query(`
ALTER TABLE user_documents ALTER TABLE user_documents
MODIFY COLUMN object_storage_id VARCHAR(255) NULL; MODIFY COLUMN object_storage_id VARCHAR(255) NULL;
@ -420,7 +434,7 @@ const createDatabase = async () => {
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
type VARCHAR(100) NOT NULL, type VARCHAR(100) NOT NULL,
contract_type ENUM('contract','gdpr') NULL DEFAULT NULL, contract_type ENUM('contract','gdpr','abo') NULL DEFAULT NULL,
storageKey VARCHAR(255) NOT NULL, storageKey VARCHAR(255) NOT NULL,
description TEXT, description TEXT,
lang VARCHAR(10) NOT NULL, lang VARCHAR(10) NOT NULL,
@ -431,12 +445,87 @@ const createDatabase = async () => {
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CHECK ( CHECK (
(type <> 'contract' AND contract_type IS NULL) (type <> 'contract' AND contract_type IS NULL)
OR (type = 'contract' AND contract_type IN ('contract','gdpr')) OR (type = 'contract' AND contract_type IN ('contract','gdpr','abo'))
) )
); );
`); `);
console.log('✅ Document templates table created/verified'); console.log('✅ Document templates table created/verified');
// Ensure enum includes 'abo' on existing schemas
try {
await connection.query(`
ALTER TABLE document_templates
MODIFY COLUMN contract_type ENUM('contract','gdpr','abo') NULL DEFAULT NULL;
`);
console.log("🔧 Ensured document_templates.contract_type includes 'abo'");
} catch (e) {
console.log(" document_templates.contract_type ENUM ALTER skipped:", e.message);
}
// Ensure CHECK constraint includes 'abo' (best-effort; some MySQL/MariaDB versions ignore/limit CHECK)
try {
const [checks] = await connection.query(`
SELECT tc.CONSTRAINT_NAME AS name, cc.CHECK_CLAUSE AS clause
FROM information_schema.TABLE_CONSTRAINTS tc
JOIN information_schema.CHECK_CONSTRAINTS cc
ON tc.CONSTRAINT_SCHEMA = cc.CONSTRAINT_SCHEMA
AND tc.CONSTRAINT_NAME = cc.CONSTRAINT_NAME
WHERE tc.CONSTRAINT_SCHEMA = DATABASE()
AND tc.TABLE_NAME = 'document_templates'
AND tc.CONSTRAINT_TYPE = 'CHECK'
`);
const checkRows = Array.isArray(checks) ? checks : [];
const toDrop = checkRows.filter(r => {
const clause = (r && r.clause) ? String(r.clause) : '';
const looksLikeOldContractTypeCheck = clause.includes('contract_type')
&& clause.includes("'contract'")
&& clause.includes("'gdpr'")
&& !clause.includes("'abo'");
return looksLikeOldContractTypeCheck;
});
if (toDrop.length) {
for (const r of toDrop) {
const name = r && r.name ? String(r.name) : '';
if (!name) continue;
try {
// MariaDB: DROP CONSTRAINT; MySQL: DROP CHECK
try {
await connection.query(`ALTER TABLE document_templates DROP CONSTRAINT \`${name}\`;`);
} catch (e1) {
await connection.query(`ALTER TABLE document_templates DROP CHECK \`${name}\`;`);
}
} catch (e2) {
console.log(' document_templates CHECK drop skipped:', e2.message);
}
}
}
// Add (or re-add) the desired check constraint
const hasDesired = checkRows.some(r => {
const clause = (r && r.clause) ? String(r.clause) : '';
return clause.includes("contract_type IN ('contract','gdpr','abo')");
});
if (!hasDesired) {
try {
await connection.query(`
ALTER TABLE document_templates
ADD CONSTRAINT chk_document_templates_contract_type
CHECK (
(type <> 'contract' AND contract_type IS NULL)
OR (type = 'contract' AND contract_type IN ('contract','gdpr','abo'))
);
`);
console.log("🔧 Ensured document_templates CHECK allows 'abo'");
} catch (e) {
console.log(' document_templates CHECK add skipped:', e.message);
}
}
} catch (e) {
console.log(' document_templates CHECK migration skipped:', e.message);
}
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS no_user_abo_mails ( CREATE TABLE IF NOT EXISTS no_user_abo_mails (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@ -724,6 +813,8 @@ const createDatabase = async () => {
company_street VARCHAR(255) NOT NULL DEFAULT '', company_street VARCHAR(255) NOT NULL DEFAULT '',
company_postal_city VARCHAR(255) NOT NULL DEFAULT '', company_postal_city VARCHAR(255) NOT NULL DEFAULT '',
company_country VARCHAR(100) NOT NULL DEFAULT 'Germany', company_country VARCHAR(100) NOT NULL DEFAULT 'Germany',
qr_code_60_base64 LONGTEXT NULL,
qr_code_120_base64 LONGTEXT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CHECK (id = 1) CHECK (id = 1)
); );
@ -734,6 +825,97 @@ const createDatabase = async () => {
`); `);
console.log('✅ Company settings table created/verified'); 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');
// --- Dashboard Platforms (admin managed dashboard cards) ---
await connection.query(`
CREATE TABLE IF NOT EXISTS dashboard_plattforms (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT NULL,
href VARCHAR(1024) NOT NULL,
icon VARCHAR(255) NULL,
color VARCHAR(255) NULL,
state BOOLEAN NOT NULL DEFAULT TRUE,
disabled BOOLEAN NOT NULL DEFAULT FALSE,
disabled_text VARCHAR(255) NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
`);
console.log('✅ Dashboard plattforms table created/verified');
await ensureIndex(connection, 'dashboard_plattforms', 'idx_dashboard_plattforms_state', '`state`');
await ensureIndex(connection, 'dashboard_plattforms', 'idx_dashboard_plattforms_sort_order', '`sort_order`');
await ensureIndex(connection, 'dashboard_plattforms', 'idx_dashboard_plattforms_title', '`title`');
// Optional one-time migration from legacy company_settings.dashboard_plattforms (JSON column)
try {
const hasLegacyColumn = await columnExists(connection, 'company_settings', 'dashboard_plattforms');
if (hasLegacyColumn) {
const [countRows] = await connection.query(`SELECT COUNT(*) AS c FROM dashboard_plattforms`);
const count = Number(countRows?.[0]?.c ?? 0);
if (count === 0) {
const [rows] = await connection.query(`SELECT dashboard_plattforms FROM company_settings WHERE id = 1`);
const raw = rows?.[0]?.dashboard_plattforms;
let parsed = [];
try {
if (raw == null) parsed = [];
else if (typeof raw === 'string') parsed = JSON.parse(raw);
else parsed = raw;
} catch (_) {
parsed = [];
}
const list = Array.isArray(parsed) ? parsed : [];
if (list.length) {
for (const p of list) {
const id = typeof p?.id === 'string' ? p.id.trim() : '';
if (!id) continue;
const title = typeof p?.title === 'string' ? p.title : '';
const href = typeof p?.href === 'string' ? p.href : '';
await connection.query(
`INSERT INTO dashboard_plattforms
(id, title, description, href, icon, color, state, disabled, disabled_text, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
title = VALUES(title),
description = VALUES(description),
href = VALUES(href),
icon = VALUES(icon),
color = VALUES(color),
state = VALUES(state),
disabled = VALUES(disabled),
disabled_text = VALUES(disabled_text),
sort_order = VALUES(sort_order)`,
[
id,
title,
typeof p?.description === 'string' ? p.description : (p?.description == null ? '' : String(p.description)),
href,
typeof p?.icon === 'string' ? p.icon : (p?.icon == null ? '' : String(p.icon)),
typeof p?.color === 'string' ? p.color : (p?.color == null ? '' : String(p.color)),
p?.state === false ? 0 : 1,
p?.disabled === true ? 1 : 0,
p?.disabledText == null ? null : String(p.disabledText),
Number.isFinite(Number(p?.sortOrder)) ? Number(p.sortOrder) : 0,
]
);
}
console.log('🔁 Migrated legacy dashboard_plattforms JSON into dashboard_plattforms table');
}
}
}
} catch (e) {
console.warn('⚠️ Dashboard platforms migration skipped/failed:', e?.message || e);
}
// --- Rate Limiting Table --- // --- Rate Limiting Table ---
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS rate_limit ( CREATE TABLE IF NOT EXISTS rate_limit (
@ -853,6 +1035,28 @@ const createDatabase = async () => {
`); `);
console.log('✅ Coffee table (simplified) created/verified'); console.log('✅ Coffee table (simplified) created/verified');
// --- Coffee shipping fees (fixed package sizes) ---
await connection.query(`
CREATE TABLE IF NOT EXISTS coffee_shipping_fees (
id INT AUTO_INCREMENT PRIMARY KEY,
piece_count INT NOT NULL,
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uq_coffee_shipping_fees_piece_count UNIQUE (piece_count),
CONSTRAINT chk_coffee_shipping_fees_piece_count CHECK (piece_count IN (60, 120))
);
`);
await connection.query(`
INSERT INTO coffee_shipping_fees (piece_count, price)
VALUES
(60, 0.00),
(120, 0.00)
ON DUPLICATE KEY UPDATE
piece_count = VALUES(piece_count)
`);
console.log('✅ Coffee shipping fees table created/verified and seeded (60/120)');
// --- Coffee Abonements (subscriptions) --- // --- Coffee Abonements (subscriptions) ---
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS coffee_abonements ( CREATE TABLE IF NOT EXISTS coffee_abonements (
@ -921,6 +1125,30 @@ const createDatabase = async () => {
ADD CONSTRAINT \`fk_abon_purchaser_user\` FOREIGN KEY (\`purchaser_user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE SET NULL ON UPDATE CASCADE` ADD CONSTRAINT \`fk_abon_purchaser_user\` FOREIGN KEY (\`purchaser_user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE SET NULL ON UPDATE CASCADE`
); );
// Contract fields
await addColumnIfMissing(connection, 'coffee_abonements', 'contract_number', `VARCHAR(50) NULL AFTER purchaser_user_id`);
await addColumnIfMissing(connection, 'coffee_abonements', 'contract_storage_key', `VARCHAR(255) NULL AFTER contract_number`);
// Additional shipping / contact fields
await addColumnIfMissing(connection, 'coffee_abonements', 'phone', `VARCHAR(50) NULL AFTER country`);
// Contract recipient
await addColumnIfMissing(connection, 'coffee_abonements', 'recipient_name', `VARCHAR(255) NULL AFTER phone`);
await addColumnIfMissing(connection, 'coffee_abonements', 'recipient_address', `TEXT NULL AFTER recipient_name`);
// Payment
await addColumnIfMissing(connection, 'coffee_abonements', 'payment_method', `VARCHAR(30) NULL AFTER frequency`);
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_by_email', `TINYINT(1) NOT NULL DEFAULT 0 AFTER payment_method`);
// Invoice address
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_same_as_shipping', `TINYINT(1) NOT NULL DEFAULT 1 AFTER invoice_by_email`);
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_full_name', `VARCHAR(200) NULL AFTER invoice_same_as_shipping`);
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_street', `VARCHAR(255) NULL AFTER invoice_full_name`);
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_postal_code', `VARCHAR(20) NULL AFTER invoice_street`);
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_city', `VARCHAR(100) NULL AFTER invoice_postal_code`);
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_phone', `VARCHAR(50) NULL AFTER invoice_city`);
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_email', `VARCHAR(255) NULL AFTER invoice_phone`);
// --- Coffee Abonement History --- // --- Coffee Abonement History ---
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS coffee_abonement_history ( CREATE TABLE IF NOT EXISTS coffee_abonement_history (
@ -1067,7 +1295,11 @@ const createDatabase = async () => {
console.log(' position type change skipped:', e.message); console.log(' position type change skipped:', e.message);
} }
try { try {
await connection.query(`ALTER TABLE user_tree_edges DROP CHECK chk_position`); try {
await connection.query('ALTER TABLE user_tree_edges DROP CONSTRAINT chk_position');
} catch (e1) {
await connection.query('ALTER TABLE user_tree_edges DROP CHECK chk_position');
}
console.log('🧹 Dropped CHECK constraint chk_position on user_tree_edges'); console.log('🧹 Dropped CHECK constraint chk_position on user_tree_edges');
} catch (e) { } catch (e) {
// MySQL versions or engines may report different messages if CHECK is not enforced or named differently // MySQL versions or engines may report different messages if CHECK is not enforced or named differently
@ -1454,7 +1686,11 @@ const createDatabase = async () => {
// Remove singleton constraint if present (best effort) // Remove singleton constraint if present (best effort)
try { try {
await connection.query(`ALTER TABLE matrix_config DROP CHECK chk_matrix_singleton`); try {
await connection.query('ALTER TABLE matrix_config DROP CONSTRAINT chk_matrix_singleton');
} catch (e1) {
await connection.query('ALTER TABLE matrix_config DROP CHECK chk_matrix_singleton');
}
console.log('🧹 Dropped chk_matrix_singleton'); console.log('🧹 Dropped chk_matrix_singleton');
} catch (e) { } catch (e) {
console.log(' chk_matrix_singleton drop skipped:', e.message); console.log(' chk_matrix_singleton drop skipped:', e.message);

View File

@ -1,155 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invoice {{invoiceNumber}}</title>
<style>
body {
font-family: Arial, sans-serif;
color: #1f2937;
background: #f8fafc;
margin: 0;
padding: 24px;
}
.card {
max-width: 760px;
margin: 0 auto;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.header {
background: #0f172a;
color: #ffffff;
padding: 20px 24px;
}
.header h1 {
margin: 0;
font-size: 22px;
}
.content {
padding: 24px;
}
.meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 20px;
}
.box {
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 12px;
background: #f9fafb;
}
.label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.value {
font-size: 14px;
font-weight: 600;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
margin-bottom: 20px;
}
th, td {
text-align: left;
padding: 10px 8px;
border-bottom: 1px solid #e5e7eb;
font-size: 14px;
}
th {
background: #f3f4f6;
font-weight: 700;
}
.totals {
margin-left: auto;
width: 320px;
border: 1px solid #e5e7eb;
border-radius: 10px;
overflow: hidden;
}
.totals-row {
display: flex;
justify-content: space-between;
padding: 10px 12px;
border-bottom: 1px solid #e5e7eb;
font-size: 14px;
}
.totals-row:last-child {
border-bottom: none;
background: #f3f4f6;
font-weight: 800;
}
.footer {
font-size: 12px;
color: #6b7280;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="card">
<div class="header">
<h1>Invoice</h1>
</div>
<div class="content">
<div class="meta">
<div class="box">
<div class="label">Invoice Number</div>
<div class="value">{{invoiceNumber}}</div>
</div>
<div class="box">
<div class="label">Issued At</div>
<div class="value">{{issuedAt}}</div>
</div>
<div class="box" style="grid-column: 1 / -1;">
<div class="label">Customer</div>
<div class="value">{{customerName}}</div>
</div>
</div>
<table>
<thead>
<tr>
<th>Item</th>
<th>Qty</th>
<th>Unit Net</th>
<th>Line Net</th>
</tr>
</thead>
<tbody>
{{itemsHtml}}
</tbody>
</table>
<div class="totals">
<div class="totals-row">
<span>Total Net</span>
<span>{{totalNet}}</span>
</div>
<div class="totals-row">
<span>Total Tax</span>
<span>{{totalTax}}</span>
</div>
<div class="totals-row">
<span>Total Gross</span>
<span>{{totalGross}}</span>
</div>
</div>
<div class="footer">
Thank you for your subscription.
</div>
</div>
</div>
</body>
</html>

View File

@ -1,155 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invoice {{invoiceNumber}}</title>
<style>
body {
font-family: Arial, sans-serif;
color: #1f2937;
background: #f8fafc;
margin: 0;
padding: 24px;
}
.card {
max-width: 760px;
margin: 0 auto;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.header {
background: #0f172a;
color: #ffffff;
padding: 20px 24px;
}
.header h1 {
margin: 0;
font-size: 22px;
}
.content {
padding: 24px;
}
.meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 20px;
}
.box {
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 12px;
background: #f9fafb;
}
.label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.value {
font-size: 14px;
font-weight: 600;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
margin-bottom: 20px;
}
th, td {
text-align: left;
padding: 10px 8px;
border-bottom: 1px solid #e5e7eb;
font-size: 14px;
}
th {
background: #f3f4f6;
font-weight: 700;
}
.totals {
margin-left: auto;
width: 320px;
border: 1px solid #e5e7eb;
border-radius: 10px;
overflow: hidden;
}
.totals-row {
display: flex;
justify-content: space-between;
padding: 10px 12px;
border-bottom: 1px solid #e5e7eb;
font-size: 14px;
}
.totals-row:last-child {
border-bottom: none;
background: #f3f4f6;
font-weight: 800;
}
.footer {
font-size: 12px;
color: #6b7280;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="card">
<div class="header">
<h1>Invoice</h1>
</div>
<div class="content">
<div class="meta">
<div class="box">
<div class="label">Invoice Number</div>
<div class="value">{{invoiceNumber}}</div>
</div>
<div class="box">
<div class="label">Issued At</div>
<div class="value">{{issuedAt}}</div>
</div>
<div class="box" style="grid-column: 1 / -1;">
<div class="label">Customer</div>
<div class="value">{{customerName}}</div>
</div>
</div>
<table>
<thead>
<tr>
<th>Item</th>
<th>Qty</th>
<th>Unit Net</th>
<th>Line Net</th>
</tr>
</thead>
<tbody>
{{itemsHtml}}
</tbody>
</table>
<div class="totals">
<div class="totals-row">
<span>Total Net</span>
<span>{{totalNet}}</span>
</div>
<div class="totals-row">
<span>Total Tax</span>
<span>{{totalTax}}</span>
</div>
<div class="totals-row">
<span>Total Gross</span>
<span>{{totalGross}}</span>
</div>
</div>
<div class="footer">
Thank you for your subscription.
</div>
</div>
</div>
</body>
</html>

View File

@ -1,209 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VERTRIEBSPARTNER / BUSINESSPARTNER / AFFILIATE - VERTRAG</title>
<style>
/* PDF friendly page setup */
@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:28%; white-space:nowrap; }
.meta-info tr:last-child td { border-bottom:0; }
/* Unified signatures */
.signatures { display:flex; gap:30px; margin-top:38px; }
.signature { flex:1; text-align:center; }
/* Added: extra space below company stamp so date sits clearly underneath */
.signature-company .sig-image { margin-bottom:22px !important; }
.signature-company .sig-date { margin-top:0 !important; }
.sig-block { display:flex; flex-direction:column; align-items:center; gap:6px; min-height:120px; }
.sig-image { display:block; max-width:180px !important; max-height:70px !important; height:auto !important; margin:0 auto 6px; }
.sig-date { margin-top:4px; display:block; }
.page-header-block{display:flex;flex-direction:column;align-items:flex-end;gap:3px;margin-bottom:6px;}
.print-date{font-size:0.7em;}
@media print {
body { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
}
/* ADDED: full size Profit Planet signature */
.pp-stamp,
.pp-stamp img {
max-width:none !important;
max-height:none !important;
width:auto !important;
height:auto !important;
display:block;
}
</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>VERTRIEBSPARTNER / BUSINESSPARTNER / AFFILIATE - VERTRAG</h1>
<p style="text-align:center;margin:0 0 6px;">idF 21.05.2025</p>
<p>abgeschlossen zwischen</p>
<p>Profit Planet GmbH (kurz PROFIT PLANET)<br>FN 649474i<br>Liebenauer Hauptstraße 82c<br>A-8041 Graz</p>
<p>und</p>
<p>Vertriebspartner / Businesspartner / Affiliate (kurz VP)</p>
<div class="meta-info">
<table class="meta-grid">
<tr>
<td>Vertriebspartner</td>
<td></td>
</tr>
<tr>
<td>Adresse</td>
<td></td>
</tr>
<tr>
<td>PLZ / Ort</td>
<td> </td>
</tr>
<tr>
<td>Vollständige Adresse</td>
<td></td>
</tr>
<tr>
<td>E-Mail / Telefon</td>
<td> / </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 Vertriebspartner / Businesspartner / Affiliate-Netzwerk 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 / Businesspartner / Affiliate für den Vertrieb der Dienstleistungen und Produkte zu gewinnen. Für die Vermittlung und Betreuung der von ihm akquirierten Vertriebspartner / Businesspartner / Affiliate 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 / Businesspartner / Affiliate werden</h2>
<p>2.1. Kapitalgesellschaften, Personengesellschaften und volljährige natürliche Personen können Vertriebspartner / Businesspartner / Affiliate 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 / Businesspartner / Affiliate 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 Zeichnungsbereichten 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 / Businesspartner / Affiliate 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 / Businesspartner / Affiliate.</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 allerdings in der Zusammenarbeit mit einem anderen Dienstleister oder Produktgeber in räumlicher oder zeitlicher Nähe zu Überschneidungen im Vertrieb, insbesondere bei Terminisierungen, Promotion-Auftritten (POS) oder anderen dienstleistungs- oder produktspezifischen Werbetätigkeiten kommen, so wäre dies nur nach ausdrücklicher Zustimmung durch PROFIT PLANET zulässig.</p>
<p>3.5. 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 jeweils aktuellen 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.6. Kundenverträge in Papierform sind vom VP unverzüglich, spätestens jedoch binnen 1 Woche nach Aufforderung durch PROFIT PLANET oder den Produktgeber an PROFIT PLANET auszuhändigen.</p>
<p>3.7. 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.8. Der VP ist während der Dauer dieser Vereinbarung und für die Dauer von 36 Monaten 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 von PROFIT PLANET und dessen Struktur, Geschäftspartner, Vertriebspartner / Businesspartner / Affiliate, 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 Unter-Vertriebspartner / Businesspartner / Affiliate 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, Vertriebspartner / Businesspartner / Affiliate-strukturen 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 (www.profit-planet.com) 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 dieses 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.8 verpflichtet sich der VP zur Zahlung einer verschuldens- und schadensunabhängigen Konventionalstrafe an den PROFIT PLANET von EUR 5.000,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 seine Tätigkeiten 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 haftet PROFIT PLANET nur, soweit diese auf Vorsatz oder grober Fahrlässigkeit oder auf grob schuldhafter Verletzung einer wesentlichen Vertragspflicht 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 & 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.</p>
<p>11.2. Der VP ist nur mit ausdrücklicher Zustimmung von PROFIT PLANET berechtigt, seine Vertriebsstruktur an einen Dritten zu übertragen.</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 Vertriebspartner / Businesspartner / Affiliateschaft 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>
</div>
<div class="page">
<div class="page-header"></div>
<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 (Auftraggeber)</p>
<!-- CHANGED: wrap stamp + date in a flex column with spacing to avoid overlap -->
<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 class="sig-date" 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 class="sig-image"></span>
<div style="font-size:0.75em;line-height:1.2;"></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>

View File

@ -1,229 +0,0 @@
<!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; } /* was 20mm 17mm 22mm 17mm */
body { counter-reset:page; font-size:13px; }
h1 { font-size:18px; margin:0 0 6px; }
h2 { font-size:13px; }
.page { page-break-after:always; }
.page:last-child { page-break-after:auto; }
.page-header {
display:flex;
justify-content:flex-end;
font-size:0.65em; /* slightly smaller */
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;}
/* ADDED signature sizing fix */
.sig-block { min-height:120px !important; }
/* UPDATED: show Profit Planet signature full size */
.pp-stamp,
.pp-stamp img {
max-width:none !important;
max-height:none !important;
width:auto !important;
height:auto !important;
display:block;
}
.sig-date { margin-top:4px; display:block; }
@media print {
.page { padding:12px 20px 18px !important; } /* was 25px 35px 35px */
}
</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;">
<!-- PAGE 1 -->
<div class="page" style="page-break-after:always;padding:12px 20px 18px;">
<!-- CHANGED header/date grouping -->
<div class="page-header-block">
<div class="page-header" style="display:flex;justify-content:flex-end;font-size:0.75em;"></div>
<div class="print-date" style="font-size:0.7em;text-align:right;margin-top:4px;margin-bottom:8px;">Erstellt am: {{currentDate}}</div>
</div>
<div class="heading-block">
<h1 style="margin:0 0 8px;text-align:center;">SUB-AUFTRAGSVERARBEITUNGS-VERTRAG</h1>
<p style="margin:0 0 6px;text-align:center;">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;">Profit Planet GmbH (kurz Auftraggeber)<br>
FN 649474i<br>
Liebenauer Hauptstraße 82c<br>
A-8041 Graz</p>
<p style="margin:0 0 6px;">und</p>
<p style="margin:0 0 6px;">Vertriebspartner (kurz Auftragnehmer)</p>
<div class="meta-info" style="border:1px solid #000;padding:8px 12px;margin:12px 0 25px;font-size:0.9em;">
<table class="meta-grid" style="width:100%;border-collapse:collapse;">
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;width:28%;white-space:nowrap;">Vertriebspartner</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{fullName}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">Adresse</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{address}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">PLZ / Ort</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{zip_code}} {{city}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">Vollständige Adresse</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{fullAddress}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;font-weight:bold;white-space:nowrap;border-bottom:0;">E-Mail / Telefon</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:0;">{{email}} / {{phone}}</td>
</tr>
</table>
</div>
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">1. PRÄAMBEL</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">2. DAUER, GEGENSTAND UND SPEZIFIZIERUNG DER AUFTRAGSVERARBEITUNG</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">2.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 style="margin:0 0 6px;">2.2. Aus dem Vertrag ergeben sich Gegenstand und Dauer des Auftrags sowie Art und Zweck der Verarbeitung.</p>
<p style="margin:0 0 6px;">2.3. Im Einzelnen sind insbesondere die folgenden Daten Bestandteil der Datenverarbeitung:</p>
</div>
</div>
</div>
<!-- PAGE 2 -->
<div class="page" style="page-break-after:always;padding:12px 20px 18px;">
<div class="page-header" style="display:flex;justify-content:flex-end;font-size:0.75em;"></div>
<table class="data-table" style="width:100%;border-collapse:collapse;border:1px solid #000;table-layout:fixed;font-size:0.85em;margin:0 0 12px;">
<tr>
<th style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;width:180px;">Art der Daten</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">Interessenten- und Kundendaten; Kontaktdaten beim Auftraggeber; Kontaktdaten des jeweiligen Datenverantwortlichen</td>
</tr>
<tr>
<th style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;">Art und Zweck der Datenverarbeitung</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">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 style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;">Kategorien betroffener Daten</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">3. ANWENDUNGSBEREICH UND VERANTWORTLICHKEIT</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">4. PFLICHTEN DES AUFTRAGNEHMERS</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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>
</div>
</div>
<div class="section" style="margin-bottom:10px;">
<!-- FIX: reopen subsection and place paragraphs inside -->
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.7. Der Auftragnehmer nennt dem Auftraggeber den Ansprechpartner für im Rahmen des Vertrages anfallende Datenschutzfragen.</p>
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.9. Daten, Datenträger sowie sämtliche sonstige Materialien sind nach Auftragsende auf Verlangen des Auftraggebers entweder herauszugeben oder zu löschen.</p>
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">5. PFLICHTEN DES AUFTRAGGEBERS</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">5.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 style="margin:0 0 6px;">5.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 style="margin:0 0 6px;">5.3. Der Auftraggeber nennt dem Auftragnehmer den Ansprechpartner für im Rahmen des Vertrages anfallende Datenschutzfragen.</p>
</div>
</div>
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">6. ANFRAGEN BETROFFENER PERSONEN</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">6.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 style="margin:0 0 6px;">6.2. Der Auftragnehmer haftet nicht, wenn das Ersuchen der betroffenen Person vom Auftraggeber nicht, nicht richtig oder nicht fristgerecht beantwortet wird.</p>
<p style="margin:0 0 6px;">6.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">7. NACHWEISMÖGLICHKEITEN</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">7.1. Der Auftragnehmer weist dem Auftraggeber die Einhaltung der in diesem Vertrag niedergelegten Pflichten mit geeigneten Mitteln nach.</p>
<p style="margin:0 0 6px;">7.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>
<!-- MOVED UP: Sections 8 & 9 and signatures (formerly PAGE 4) -->
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">8. SUBUNTERNEHMER (WEITERE AUFTRAGSVERARBEITER)</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">8.1. Der Einsatz von Subunternehmern als weitere Auftragsverarbeiter ist nur zulässig, wenn der Auftraggeber vorher zugestimmt hat.</p>
<p style="margin:0 0 6px;">8.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 style="margin:0 0 6px;">8.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">9. INFORMATIONSPFLICHTEN, SCHRIFTFORMKLAUSEL, RECHTSWAHL</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.4. Es gilt das auf dem Hauptvertrag anwendbare Recht sowie Gerichtsstand.</p>
</div>
</div>
<div class="signatures" style="display:flex;gap:30px;margin-top:38px;">
<div class="signature" style="flex:1;text-align:center;">
<p style="margin:0 0 6px;">Für PROFIT PLANET (Auftraggeber)</p>
<!-- CHANGED: wrap stamp + date in a flex column with spacing to avoid overlap -->
<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 class="sig-date" 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" style="flex:1;text-align:center;">
<p style="margin:0 0 6px;">Für den VP (Auftragnehmer)</p>
<div class="sig-block" style="display:flex;flex-direction:column;align-items:center;gap:4px;min-height:110px;">
<span style="display:block;max-width:100%;max-height:80px;">{{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>

View File

@ -1,229 +0,0 @@
<!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; } /* was 20mm 17mm 22mm 17mm */
body { counter-reset:page; font-size:13px; }
h1 { font-size:18px; margin:0 0 6px; }
h2 { font-size:13px; }
.page { page-break-after:always; }
.page:last-child { page-break-after:auto; }
.page-header {
display:flex;
justify-content:flex-end;
font-size:0.65em; /* slightly smaller */
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;}
/* ADDED signature sizing fix */
.sig-block { min-height:120px !important; }
/* UPDATED: show Profit Planet signature full size */
.pp-stamp,
.pp-stamp img {
max-width:none !important;
max-height:none !important;
width:auto !important;
height:auto !important;
display:block;
}
.sig-date { margin-top:4px; display:block; }
@media print {
.page { padding:12px 20px 18px !important; } /* was 25px 35px 35px */
}
</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;">
<!-- PAGE 1 -->
<div class="page" style="page-break-after:always;padding:12px 20px 18px;">
<!-- CHANGED header/date grouping -->
<div class="page-header-block">
<div class="page-header" style="display:flex;justify-content:flex-end;font-size:0.75em;"></div>
<div class="print-date" style="font-size:0.7em;text-align:right;margin-top:4px;margin-bottom:8px;">Erstellt am: {{currentDate}}</div>
</div>
<div class="heading-block">
<h1 style="margin:0 0 8px;text-align:center;">SUB-AUFTRAGSVERARBEITUNGS-VERTRAG</h1>
<p style="margin:0 0 6px;text-align:center;">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;">Profit Planet GmbH (kurz Auftraggeber)<br>
FN 649474i<br>
Liebenauer Hauptstraße 82c<br>
A-8041 Graz</p>
<p style="margin:0 0 6px;">und</p>
<p style="margin:0 0 6px;">Vertriebspartner (kurz Auftragnehmer)</p>
<div class="meta-info" style="border:1px solid #000;padding:8px 12px;margin:12px 0 25px;font-size:0.9em;">
<table class="meta-grid" style="width:100%;border-collapse:collapse;">
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;width:28%;white-space:nowrap;">Vertriebspartner</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{fullName}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">Adresse</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{address}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">PLZ / Ort</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{zip_code}} {{city}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">Vollständige Adresse</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{fullAddress}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;font-weight:bold;white-space:nowrap;border-bottom:0;">E-Mail / Telefon</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:0;">{{email}} / {{phone}}</td>
</tr>
</table>
</div>
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">1. PRÄAMBEL</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">2. DAUER, GEGENSTAND UND SPEZIFIZIERUNG DER AUFTRAGSVERARBEITUNG</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">2.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 style="margin:0 0 6px;">2.2. Aus dem Vertrag ergeben sich Gegenstand und Dauer des Auftrags sowie Art und Zweck der Verarbeitung.</p>
<p style="margin:0 0 6px;">2.3. Im Einzelnen sind insbesondere die folgenden Daten Bestandteil der Datenverarbeitung:</p>
</div>
</div>
</div>
<!-- PAGE 2 -->
<div class="page" style="page-break-after:always;padding:12px 20px 18px;">
<div class="page-header" style="display:flex;justify-content:flex-end;font-size:0.75em;"></div>
<table class="data-table" style="width:100%;border-collapse:collapse;border:1px solid #000;table-layout:fixed;font-size:0.85em;margin:0 0 12px;">
<tr>
<th style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;width:180px;">Art der Daten</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">Interessenten- und Kundendaten; Kontaktdaten beim Auftraggeber; Kontaktdaten des jeweiligen Datenverantwortlichen</td>
</tr>
<tr>
<th style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;">Art und Zweck der Datenverarbeitung</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">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 style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;">Kategorien betroffener Daten</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">3. ANWENDUNGSBEREICH UND VERANTWORTLICHKEIT</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">4. PFLICHTEN DES AUFTRAGNEHMERS</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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>
</div>
</div>
<div class="section" style="margin-bottom:10px;">
<!-- FIX: reopen subsection and place paragraphs inside -->
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.7. Der Auftragnehmer nennt dem Auftraggeber den Ansprechpartner für im Rahmen des Vertrages anfallende Datenschutzfragen.</p>
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.9. Daten, Datenträger sowie sämtliche sonstige Materialien sind nach Auftragsende auf Verlangen des Auftraggebers entweder herauszugeben oder zu löschen.</p>
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">5. PFLICHTEN DES AUFTRAGGEBERS</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">5.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 style="margin:0 0 6px;">5.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 style="margin:0 0 6px;">5.3. Der Auftraggeber nennt dem Auftragnehmer den Ansprechpartner für im Rahmen des Vertrages anfallende Datenschutzfragen.</p>
</div>
</div>
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">6. ANFRAGEN BETROFFENER PERSONEN</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">6.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 style="margin:0 0 6px;">6.2. Der Auftragnehmer haftet nicht, wenn das Ersuchen der betroffenen Person vom Auftraggeber nicht, nicht richtig oder nicht fristgerecht beantwortet wird.</p>
<p style="margin:0 0 6px;">6.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">7. NACHWEISMÖGLICHKEITEN</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">7.1. Der Auftragnehmer weist dem Auftraggeber die Einhaltung der in diesem Vertrag niedergelegten Pflichten mit geeigneten Mitteln nach.</p>
<p style="margin:0 0 6px;">7.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>
<!-- MOVED UP: Sections 8 & 9 and signatures (formerly PAGE 4) -->
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">8. SUBUNTERNEHMER (WEITERE AUFTRAGSVERARBEITER)</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">8.1. Der Einsatz von Subunternehmern als weitere Auftragsverarbeiter ist nur zulässig, wenn der Auftraggeber vorher zugestimmt hat.</p>
<p style="margin:0 0 6px;">8.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 style="margin:0 0 6px;">8.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">9. INFORMATIONSPFLICHTEN, SCHRIFTFORMKLAUSEL, RECHTSWAHL</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.4. Es gilt das auf dem Hauptvertrag anwendbare Recht sowie Gerichtsstand.</p>
</div>
</div>
<div class="signatures" style="display:flex;gap:30px;margin-top:38px;">
<div class="signature" style="flex:1;text-align:center;">
<p style="margin:0 0 6px;">Für PROFIT PLANET (Auftraggeber)</p>
<!-- CHANGED: wrap stamp + date in a flex column with spacing to avoid overlap -->
<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 class="sig-date" 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" style="flex:1;text-align:center;">
<p style="margin:0 0 6px;">Für den VP (Auftragnehmer)</p>
<div class="sig-block" style="display:flex;flex-direction:column;align-items:center;gap:4px;min-height:110px;">
<span style="display:block;max-width:100%;max-height:80px;">{{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>

View File

@ -1,229 +0,0 @@
<!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; } /* was 20mm 17mm 22mm 17mm */
body { counter-reset:page; font-size:13px; }
h1 { font-size:18px; margin:0 0 6px; }
h2 { font-size:13px; }
.page { page-break-after:always; }
.page:last-child { page-break-after:auto; }
.page-header {
display:flex;
justify-content:flex-end;
font-size:0.65em; /* slightly smaller */
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;}
/* ADDED signature sizing fix */
.sig-block { min-height:120px !important; }
/* UPDATED: show Profit Planet signature full size */
.pp-stamp,
.pp-stamp img {
max-width:none !important;
max-height:none !important;
width:auto !important;
height:auto !important;
display:block;
}
.sig-date { margin-top:4px; display:block; }
@media print {
.page { padding:12px 20px 18px !important; } /* was 25px 35px 35px */
}
</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;">
<!-- PAGE 1 -->
<div class="page" style="page-break-after:always;padding:12px 20px 18px;">
<!-- CHANGED header/date grouping -->
<div class="page-header-block">
<div class="page-header" style="display:flex;justify-content:flex-end;font-size:0.75em;"></div>
<div class="print-date" style="font-size:0.7em;text-align:right;margin-top:4px;margin-bottom:8px;">Erstellt am: {{currentDate}}</div>
</div>
<div class="heading-block">
<h1 style="margin:0 0 8px;text-align:center;">SUB-AUFTRAGSVERARBEITUNGS-VERTRAG</h1>
<p style="margin:0 0 6px;text-align:center;">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;">Profit Planet GmbH (kurz Auftraggeber)<br>
FN 649474i<br>
Liebenauer Hauptstraße 82c<br>
A-8041 Graz</p>
<p style="margin:0 0 6px;">und</p>
<p style="margin:0 0 6px;">Vertriebspartner (kurz Auftragnehmer)</p>
<div class="meta-info" style="border:1px solid #000;padding:8px 12px;margin:12px 0 25px;font-size:0.9em;">
<table class="meta-grid" style="width:100%;border-collapse:collapse;">
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;width:28%;white-space:nowrap;">Vertriebspartner</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;"></td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">Adresse</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;"></td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">PLZ / Ort</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;"> </td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">Vollständige Adresse</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;"></td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;font-weight:bold;white-space:nowrap;border-bottom:0;">E-Mail / Telefon</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:0;"> / </td>
</tr>
</table>
</div>
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">1. PRÄAMBEL</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">2. DAUER, GEGENSTAND UND SPEZIFIZIERUNG DER AUFTRAGSVERARBEITUNG</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">2.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 style="margin:0 0 6px;">2.2. Aus dem Vertrag ergeben sich Gegenstand und Dauer des Auftrags sowie Art und Zweck der Verarbeitung.</p>
<p style="margin:0 0 6px;">2.3. Im Einzelnen sind insbesondere die folgenden Daten Bestandteil der Datenverarbeitung:</p>
</div>
</div>
</div>
<!-- PAGE 2 -->
<div class="page" style="page-break-after:always;padding:12px 20px 18px;">
<div class="page-header" style="display:flex;justify-content:flex-end;font-size:0.75em;"></div>
<table class="data-table" style="width:100%;border-collapse:collapse;border:1px solid #000;table-layout:fixed;font-size:0.85em;margin:0 0 12px;">
<tr>
<th style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;width:180px;">Art der Daten</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">Interessenten- und Kundendaten; Kontaktdaten beim Auftraggeber; Kontaktdaten des jeweiligen Datenverantwortlichen</td>
</tr>
<tr>
<th style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;">Art und Zweck der Datenverarbeitung</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">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 style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;">Kategorien betroffener Daten</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">3. ANWENDUNGSBEREICH UND VERANTWORTLICHKEIT</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">4. PFLICHTEN DES AUFTRAGNEHMERS</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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>
</div>
</div>
<div class="section" style="margin-bottom:10px;">
<!-- FIX: reopen subsection and place paragraphs inside -->
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.7. Der Auftragnehmer nennt dem Auftraggeber den Ansprechpartner für im Rahmen des Vertrages anfallende Datenschutzfragen.</p>
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.9. Daten, Datenträger sowie sämtliche sonstige Materialien sind nach Auftragsende auf Verlangen des Auftraggebers entweder herauszugeben oder zu löschen.</p>
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">5. PFLICHTEN DES AUFTRAGGEBERS</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">5.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 style="margin:0 0 6px;">5.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 style="margin:0 0 6px;">5.3. Der Auftraggeber nennt dem Auftragnehmer den Ansprechpartner für im Rahmen des Vertrages anfallende Datenschutzfragen.</p>
</div>
</div>
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">6. ANFRAGEN BETROFFENER PERSONEN</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">6.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 style="margin:0 0 6px;">6.2. Der Auftragnehmer haftet nicht, wenn das Ersuchen der betroffenen Person vom Auftraggeber nicht, nicht richtig oder nicht fristgerecht beantwortet wird.</p>
<p style="margin:0 0 6px;">6.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">7. NACHWEISMÖGLICHKEITEN</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">7.1. Der Auftragnehmer weist dem Auftraggeber die Einhaltung der in diesem Vertrag niedergelegten Pflichten mit geeigneten Mitteln nach.</p>
<p style="margin:0 0 6px;">7.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>
<!-- MOVED UP: Sections 8 & 9 and signatures (formerly PAGE 4) -->
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">8. SUBUNTERNEHMER (WEITERE AUFTRAGSVERARBEITER)</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">8.1. Der Einsatz von Subunternehmern als weitere Auftragsverarbeiter ist nur zulässig, wenn der Auftraggeber vorher zugestimmt hat.</p>
<p style="margin:0 0 6px;">8.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 style="margin:0 0 6px;">8.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">9. INFORMATIONSPFLICHTEN, SCHRIFTFORMKLAUSEL, RECHTSWAHL</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.4. Es gilt das auf dem Hauptvertrag anwendbare Recht sowie Gerichtsstand.</p>
</div>
</div>
<div class="signatures" style="display:flex;gap:30px;margin-top:38px;">
<div class="signature" style="flex:1;text-align:center;">
<p style="margin:0 0 6px;">Für PROFIT PLANET (Auftraggeber)</p>
<!-- CHANGED: wrap stamp + date in a flex column with spacing to avoid overlap -->
<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 class="sig-date" 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" style="flex:1;text-align:center;">
<p style="margin:0 0 6px;">Für den VP (Auftragnehmer)</p>
<div class="sig-block" style="display:flex;flex-direction:column;align-items:center;gap:4px;min-height:110px;">
<span style="display:block;max-width:100%;max-height:80px;"></span>
<div style="font-size:0.75em;line-height:1.2;"></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>

View File

@ -1,209 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VERTRIEBSPARTNER / BUSINESSPARTNER / AFFILIATE - VERTRAG</title>
<style>
/* PDF friendly page setup */
@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:28%; white-space:nowrap; }
.meta-info tr:last-child td { border-bottom:0; }
/* Unified signatures */
.signatures { display:flex; gap:30px; margin-top:38px; }
.signature { flex:1; text-align:center; }
/* Added: extra space below company stamp so date sits clearly underneath */
.signature-company .sig-image { margin-bottom:22px !important; }
.signature-company .sig-date { margin-top:0 !important; }
.sig-block { display:flex; flex-direction:column; align-items:center; gap:6px; min-height:120px; }
.sig-image { display:block; max-width:180px !important; max-height:70px !important; height:auto !important; margin:0 auto 6px; }
.sig-date { margin-top:4px; display:block; }
.page-header-block{display:flex;flex-direction:column;align-items:flex-end;gap:3px;margin-bottom:6px;}
.print-date{font-size:0.7em;}
@media print {
body { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
}
/* ADDED: full size Profit Planet signature */
.pp-stamp,
.pp-stamp img {
max-width:none !important;
max-height:none !important;
width:auto !important;
height:auto !important;
display:block;
}
</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>VERTRIEBSPARTNER / BUSINESSPARTNER / AFFILIATE - VERTRAG</h1>
<p style="text-align:center;margin:0 0 6px;">idF 21.05.2025</p>
<p>abgeschlossen zwischen</p>
<p>Profit Planet GmbH (kurz PROFIT PLANET)<br>FN 649474i<br>Liebenauer Hauptstraße 82c<br>A-8041 Graz</p>
<p>und</p>
<p>Vertriebspartner / Businesspartner / Affiliate (kurz VP)</p>
<div class="meta-info">
<table class="meta-grid">
<tr>
<td>Vertriebspartner</td>
<td>{{fullName}}</td>
</tr>
<tr>
<td>Adresse</td>
<td>{{address}}</td>
</tr>
<tr>
<td>PLZ / Ort</td>
<td>{{zip_code}} {{city}}</td>
</tr>
<tr>
<td>Vollständige Adresse</td>
<td>{{fullAddress}}</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 Vertriebspartner / Businesspartner / Affiliate-Netzwerk 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 / Businesspartner / Affiliate für den Vertrieb der Dienstleistungen und Produkte zu gewinnen. Für die Vermittlung und Betreuung der von ihm akquirierten Vertriebspartner / Businesspartner / Affiliate 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 / Businesspartner / Affiliate werden</h2>
<p>2.1. Kapitalgesellschaften, Personengesellschaften und volljährige natürliche Personen können Vertriebspartner / Businesspartner / Affiliate 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 / Businesspartner / Affiliate 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 Zeichnungsbereichten 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 / Businesspartner / Affiliate 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 / Businesspartner / Affiliate.</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 allerdings in der Zusammenarbeit mit einem anderen Dienstleister oder Produktgeber in räumlicher oder zeitlicher Nähe zu Überschneidungen im Vertrieb, insbesondere bei Terminisierungen, Promotion-Auftritten (POS) oder anderen dienstleistungs- oder produktspezifischen Werbetätigkeiten kommen, so wäre dies nur nach ausdrücklicher Zustimmung durch PROFIT PLANET zulässig.</p>
<p>3.5. 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 jeweils aktuellen 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.6. Kundenverträge in Papierform sind vom VP unverzüglich, spätestens jedoch binnen 1 Woche nach Aufforderung durch PROFIT PLANET oder den Produktgeber an PROFIT PLANET auszuhändigen.</p>
<p>3.7. 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.8. Der VP ist während der Dauer dieser Vereinbarung und für die Dauer von 36 Monaten 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 von PROFIT PLANET und dessen Struktur, Geschäftspartner, Vertriebspartner / Businesspartner / Affiliate, 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 Unter-Vertriebspartner / Businesspartner / Affiliate 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, Vertriebspartner / Businesspartner / Affiliate-strukturen 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 (www.profit-planet.com) 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 dieses 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.8 verpflichtet sich der VP zur Zahlung einer verschuldens- und schadensunabhängigen Konventionalstrafe an den PROFIT PLANET von EUR 5.000,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 seine Tätigkeiten 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 haftet PROFIT PLANET nur, soweit diese auf Vorsatz oder grober Fahrlässigkeit oder auf grob schuldhafter Verletzung einer wesentlichen Vertragspflicht 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 & 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.</p>
<p>11.2. Der VP ist nur mit ausdrücklicher Zustimmung von PROFIT PLANET berechtigt, seine Vertriebsstruktur an einen Dritten zu übertragen.</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 Vertriebspartner / Businesspartner / Affiliateschaft 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>
</div>
<div class="page">
<div class="page-header"></div>
<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 (Auftraggeber)</p>
<!-- CHANGED: wrap stamp + date in a flex column with spacing to avoid overlap -->
<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 class="sig-date" 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 class="sig-image">{{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>

View File

@ -1,209 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VERTRIEBSPARTNER / BUSINESSPARTNER / AFFILIATE - VERTRAG</title>
<style>
/* PDF friendly page setup */
@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:28%; white-space:nowrap; }
.meta-info tr:last-child td { border-bottom:0; }
/* Unified signatures */
.signatures { display:flex; gap:30px; margin-top:38px; }
.signature { flex:1; text-align:center; }
/* Added: extra space below company stamp so date sits clearly underneath */
.signature-company .sig-image { margin-bottom:22px !important; }
.signature-company .sig-date { margin-top:0 !important; }
.sig-block { display:flex; flex-direction:column; align-items:center; gap:6px; min-height:120px; }
.sig-image { display:block; max-width:180px !important; max-height:70px !important; height:auto !important; margin:0 auto 6px; }
.sig-date { margin-top:4px; display:block; }
.page-header-block{display:flex;flex-direction:column;align-items:flex-end;gap:3px;margin-bottom:6px;}
.print-date{font-size:0.7em;}
@media print {
body { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
}
/* ADDED: full size Profit Planet signature */
.pp-stamp,
.pp-stamp img {
max-width:none !important;
max-height:none !important;
width:auto !important;
height:auto !important;
display:block;
}
</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>VERTRIEBSPARTNER / BUSINESSPARTNER / AFFILIATE - VERTRAG</h1>
<p style="text-align:center;margin:0 0 6px;">idF 21.05.2025</p>
<p>abgeschlossen zwischen</p>
<p>Profit Planet GmbH (kurz PROFIT PLANET)<br>FN 649474i<br>Liebenauer Hauptstraße 82c<br>A-8041 Graz</p>
<p>und</p>
<p>Vertriebspartner / Businesspartner / Affiliate (kurz VP)</p>
<div class="meta-info">
<table class="meta-grid">
<tr>
<td>Vertriebspartner</td>
<td>{{fullName}}</td>
</tr>
<tr>
<td>Adresse</td>
<td>{{address}}</td>
</tr>
<tr>
<td>PLZ / Ort</td>
<td>{{zip_code}} {{city}}</td>
</tr>
<tr>
<td>Vollständige Adresse</td>
<td>{{fullAddress}}</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 Vertriebspartner / Businesspartner / Affiliate-Netzwerk 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 / Businesspartner / Affiliate für den Vertrieb der Dienstleistungen und Produkte zu gewinnen. Für die Vermittlung und Betreuung der von ihm akquirierten Vertriebspartner / Businesspartner / Affiliate 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 / Businesspartner / Affiliate werden</h2>
<p>2.1. Kapitalgesellschaften, Personengesellschaften und volljährige natürliche Personen können Vertriebspartner / Businesspartner / Affiliate 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 / Businesspartner / Affiliate 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 Zeichnungsbereichten 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 / Businesspartner / Affiliate 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 / Businesspartner / Affiliate.</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 allerdings in der Zusammenarbeit mit einem anderen Dienstleister oder Produktgeber in räumlicher oder zeitlicher Nähe zu Überschneidungen im Vertrieb, insbesondere bei Terminisierungen, Promotion-Auftritten (POS) oder anderen dienstleistungs- oder produktspezifischen Werbetätigkeiten kommen, so wäre dies nur nach ausdrücklicher Zustimmung durch PROFIT PLANET zulässig.</p>
<p>3.5. 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 jeweils aktuellen 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.6. Kundenverträge in Papierform sind vom VP unverzüglich, spätestens jedoch binnen 1 Woche nach Aufforderung durch PROFIT PLANET oder den Produktgeber an PROFIT PLANET auszuhändigen.</p>
<p>3.7. 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.8. Der VP ist während der Dauer dieser Vereinbarung und für die Dauer von 36 Monaten 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 von PROFIT PLANET und dessen Struktur, Geschäftspartner, Vertriebspartner / Businesspartner / Affiliate, 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 Unter-Vertriebspartner / Businesspartner / Affiliate 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, Vertriebspartner / Businesspartner / Affiliate-strukturen 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 (www.profit-planet.com) 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 dieses 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.8 verpflichtet sich der VP zur Zahlung einer verschuldens- und schadensunabhängigen Konventionalstrafe an den PROFIT PLANET von EUR 5.000,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 seine Tätigkeiten 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 haftet PROFIT PLANET nur, soweit diese auf Vorsatz oder grober Fahrlässigkeit oder auf grob schuldhafter Verletzung einer wesentlichen Vertragspflicht 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 & 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.</p>
<p>11.2. Der VP ist nur mit ausdrücklicher Zustimmung von PROFIT PLANET berechtigt, seine Vertriebsstruktur an einen Dritten zu übertragen.</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 Vertriebspartner / Businesspartner / Affiliateschaft 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>
</div>
<div class="page">
<div class="page-header"></div>
<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 (Auftraggeber)</p>
<!-- CHANGED: wrap stamp + date in a flex column with spacing to avoid overlap -->
<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 class="sig-date" 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 class="sig-image">{{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>

View File

@ -1,229 +0,0 @@
<!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; } /* was 20mm 17mm 22mm 17mm */
body { counter-reset:page; font-size:13px; }
h1 { font-size:18px; margin:0 0 6px; }
h2 { font-size:13px; }
.page { page-break-after:always; }
.page:last-child { page-break-after:auto; }
.page-header {
display:flex;
justify-content:flex-end;
font-size:0.65em; /* slightly smaller */
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;}
/* ADDED signature sizing fix */
.sig-block { min-height:120px !important; }
/* UPDATED: show Profit Planet signature full size */
.pp-stamp,
.pp-stamp img {
max-width:none !important;
max-height:none !important;
width:auto !important;
height:auto !important;
display:block;
}
.sig-date { margin-top:4px; display:block; }
@media print {
.page { padding:12px 20px 18px !important; } /* was 25px 35px 35px */
}
</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;">
<!-- PAGE 1 -->
<div class="page" style="page-break-after:always;padding:12px 20px 18px;">
<!-- CHANGED header/date grouping -->
<div class="page-header-block">
<div class="page-header" style="display:flex;justify-content:flex-end;font-size:0.75em;"></div>
<div class="print-date" style="font-size:0.7em;text-align:right;margin-top:4px;margin-bottom:8px;">Erstellt am: {{currentDate}}</div>
</div>
<div class="heading-block">
<h1 style="margin:0 0 8px;text-align:center;">SUB-AUFTRAGSVERARBEITUNGS-VERTRAG</h1>
<p style="margin:0 0 6px;text-align:center;">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;">Profit Planet GmbH (kurz Auftraggeber)<br>
FN 649474i<br>
Liebenauer Hauptstraße 82c<br>
A-8041 Graz</p>
<p style="margin:0 0 6px;">und</p>
<p style="margin:0 0 6px;">Vertriebspartner (kurz Auftragnehmer)</p>
<div class="meta-info" style="border:1px solid #000;padding:8px 12px;margin:12px 0 25px;font-size:0.9em;">
<table class="meta-grid" style="width:100%;border-collapse:collapse;">
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;width:28%;white-space:nowrap;">Vertriebspartner</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{fullName}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">Adresse</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{address}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">PLZ / Ort</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{zip_code}} {{city}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">Vollständige Adresse</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{fullAddress}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;font-weight:bold;white-space:nowrap;border-bottom:0;">E-Mail / Telefon</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:0;">{{email}} / {{phone}}</td>
</tr>
</table>
</div>
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">1. PRÄAMBEL</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">2. DAUER, GEGENSTAND UND SPEZIFIZIERUNG DER AUFTRAGSVERARBEITUNG</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">2.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 style="margin:0 0 6px;">2.2. Aus dem Vertrag ergeben sich Gegenstand und Dauer des Auftrags sowie Art und Zweck der Verarbeitung.</p>
<p style="margin:0 0 6px;">2.3. Im Einzelnen sind insbesondere die folgenden Daten Bestandteil der Datenverarbeitung:</p>
</div>
</div>
</div>
<!-- PAGE 2 -->
<div class="page" style="page-break-after:always;padding:12px 20px 18px;">
<div class="page-header" style="display:flex;justify-content:flex-end;font-size:0.75em;"></div>
<table class="data-table" style="width:100%;border-collapse:collapse;border:1px solid #000;table-layout:fixed;font-size:0.85em;margin:0 0 12px;">
<tr>
<th style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;width:180px;">Art der Daten</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">Interessenten- und Kundendaten; Kontaktdaten beim Auftraggeber; Kontaktdaten des jeweiligen Datenverantwortlichen</td>
</tr>
<tr>
<th style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;">Art und Zweck der Datenverarbeitung</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">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 style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;">Kategorien betroffener Daten</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">3. ANWENDUNGSBEREICH UND VERANTWORTLICHKEIT</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">4. PFLICHTEN DES AUFTRAGNEHMERS</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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>
</div>
</div>
<div class="section" style="margin-bottom:10px;">
<!-- FIX: reopen subsection and place paragraphs inside -->
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.7. Der Auftragnehmer nennt dem Auftraggeber den Ansprechpartner für im Rahmen des Vertrages anfallende Datenschutzfragen.</p>
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.9. Daten, Datenträger sowie sämtliche sonstige Materialien sind nach Auftragsende auf Verlangen des Auftraggebers entweder herauszugeben oder zu löschen.</p>
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">5. PFLICHTEN DES AUFTRAGGEBERS</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">5.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 style="margin:0 0 6px;">5.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 style="margin:0 0 6px;">5.3. Der Auftraggeber nennt dem Auftragnehmer den Ansprechpartner für im Rahmen des Vertrages anfallende Datenschutzfragen.</p>
</div>
</div>
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">6. ANFRAGEN BETROFFENER PERSONEN</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">6.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 style="margin:0 0 6px;">6.2. Der Auftragnehmer haftet nicht, wenn das Ersuchen der betroffenen Person vom Auftraggeber nicht, nicht richtig oder nicht fristgerecht beantwortet wird.</p>
<p style="margin:0 0 6px;">6.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">7. NACHWEISMÖGLICHKEITEN</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">7.1. Der Auftragnehmer weist dem Auftraggeber die Einhaltung der in diesem Vertrag niedergelegten Pflichten mit geeigneten Mitteln nach.</p>
<p style="margin:0 0 6px;">7.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>
<!-- MOVED UP: Sections 8 & 9 and signatures (formerly PAGE 4) -->
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">8. SUBUNTERNEHMER (WEITERE AUFTRAGSVERARBEITER)</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">8.1. Der Einsatz von Subunternehmern als weitere Auftragsverarbeiter ist nur zulässig, wenn der Auftraggeber vorher zugestimmt hat.</p>
<p style="margin:0 0 6px;">8.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 style="margin:0 0 6px;">8.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">9. INFORMATIONSPFLICHTEN, SCHRIFTFORMKLAUSEL, RECHTSWAHL</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.4. Es gilt das auf dem Hauptvertrag anwendbare Recht sowie Gerichtsstand.</p>
</div>
</div>
<div class="signatures" style="display:flex;gap:30px;margin-top:38px;">
<div class="signature" style="flex:1;text-align:center;">
<p style="margin:0 0 6px;">Für PROFIT PLANET (Auftraggeber)</p>
<!-- CHANGED: wrap stamp + date in a flex column with spacing to avoid overlap -->
<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 class="sig-date" 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" style="flex:1;text-align:center;">
<p style="margin:0 0 6px;">Für den VP (Auftragnehmer)</p>
<div class="sig-block" style="display:flex;flex-direction:column;align-items:center;gap:4px;min-height:110px;">
<span style="display:block;max-width:100%;max-height:80px;">{{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>

View File

@ -1,229 +0,0 @@
<!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; } /* was 20mm 17mm 22mm 17mm */
body { counter-reset:page; font-size:13px; }
h1 { font-size:18px; margin:0 0 6px; }
h2 { font-size:13px; }
.page { page-break-after:always; }
.page:last-child { page-break-after:auto; }
.page-header {
display:flex;
justify-content:flex-end;
font-size:0.65em; /* slightly smaller */
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;}
/* ADDED signature sizing fix */
.sig-block { min-height:120px !important; }
/* UPDATED: show Profit Planet signature full size */
.pp-stamp,
.pp-stamp img {
max-width:none !important;
max-height:none !important;
width:auto !important;
height:auto !important;
display:block;
}
.sig-date { margin-top:4px; display:block; }
@media print {
.page { padding:12px 20px 18px !important; } /* was 25px 35px 35px */
}
</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;">
<!-- PAGE 1 -->
<div class="page" style="page-break-after:always;padding:12px 20px 18px;">
<!-- CHANGED header/date grouping -->
<div class="page-header-block">
<div class="page-header" style="display:flex;justify-content:flex-end;font-size:0.75em;"></div>
<div class="print-date" style="font-size:0.7em;text-align:right;margin-top:4px;margin-bottom:8px;">Erstellt am: {{currentDate}}</div>
</div>
<div class="heading-block">
<h1 style="margin:0 0 8px;text-align:center;">SUB-AUFTRAGSVERARBEITUNGS-VERTRAG</h1>
<p style="margin:0 0 6px;text-align:center;">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;">Profit Planet GmbH (kurz Auftraggeber)<br>
FN 649474i<br>
Liebenauer Hauptstraße 82c<br>
A-8041 Graz</p>
<p style="margin:0 0 6px;">und</p>
<p style="margin:0 0 6px;">Vertriebspartner (kurz Auftragnehmer)</p>
<div class="meta-info" style="border:1px solid #000;padding:8px 12px;margin:12px 0 25px;font-size:0.9em;">
<table class="meta-grid" style="width:100%;border-collapse:collapse;">
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;width:28%;white-space:nowrap;">Vertriebspartner</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{fullName}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">Adresse</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{address}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">PLZ / Ort</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{zip_code}} {{city}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;font-weight:bold;white-space:nowrap;">Vollständige Adresse</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:1px solid #ccc;">{{fullAddress}}</td>
</tr>
<tr>
<td style="padding:3px 6px;vertical-align:top;font-weight:bold;white-space:nowrap;border-bottom:0;">E-Mail / Telefon</td>
<td style="padding:3px 6px;vertical-align:top;border-bottom:0;">{{email}} / {{phone}}</td>
</tr>
</table>
</div>
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">1. PRÄAMBEL</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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 style="margin:0 0 6px;">1.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">2. DAUER, GEGENSTAND UND SPEZIFIZIERUNG DER AUFTRAGSVERARBEITUNG</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">2.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 style="margin:0 0 6px;">2.2. Aus dem Vertrag ergeben sich Gegenstand und Dauer des Auftrags sowie Art und Zweck der Verarbeitung.</p>
<p style="margin:0 0 6px;">2.3. Im Einzelnen sind insbesondere die folgenden Daten Bestandteil der Datenverarbeitung:</p>
</div>
</div>
</div>
<!-- PAGE 2 -->
<div class="page" style="page-break-after:always;padding:12px 20px 18px;">
<div class="page-header" style="display:flex;justify-content:flex-end;font-size:0.75em;"></div>
<table class="data-table" style="width:100%;border-collapse:collapse;border:1px solid #000;table-layout:fixed;font-size:0.85em;margin:0 0 12px;">
<tr>
<th style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;width:180px;">Art der Daten</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">Interessenten- und Kundendaten; Kontaktdaten beim Auftraggeber; Kontaktdaten des jeweiligen Datenverantwortlichen</td>
</tr>
<tr>
<th style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;">Art und Zweck der Datenverarbeitung</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">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 style="border:1px solid #000;padding:6px 8px;vertical-align:top;background:#f5f5f5;">Kategorien betroffener Daten</th>
<td style="border:1px solid #000;padding:6px 8px;vertical-align:top;">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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">3. ANWENDUNGSBEREICH UND VERANTWORTLICHKEIT</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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 style="margin:0 0 6px;">3.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">4. PFLICHTEN DES AUFTRAGNEHMERS</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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>
</div>
</div>
<div class="section" style="margin-bottom:10px;">
<!-- FIX: reopen subsection and place paragraphs inside -->
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.7. Der Auftragnehmer nennt dem Auftraggeber den Ansprechpartner für im Rahmen des Vertrages anfallende Datenschutzfragen.</p>
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.9. Daten, Datenträger sowie sämtliche sonstige Materialien sind nach Auftragsende auf Verlangen des Auftraggebers entweder herauszugeben oder zu löschen.</p>
<p style="margin:0 0 6px;">4.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 style="margin:0 0 6px;">4.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">5. PFLICHTEN DES AUFTRAGGEBERS</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">5.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 style="margin:0 0 6px;">5.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 style="margin:0 0 6px;">5.3. Der Auftraggeber nennt dem Auftragnehmer den Ansprechpartner für im Rahmen des Vertrages anfallende Datenschutzfragen.</p>
</div>
</div>
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">6. ANFRAGEN BETROFFENER PERSONEN</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">6.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 style="margin:0 0 6px;">6.2. Der Auftragnehmer haftet nicht, wenn das Ersuchen der betroffenen Person vom Auftraggeber nicht, nicht richtig oder nicht fristgerecht beantwortet wird.</p>
<p style="margin:0 0 6px;">6.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">7. NACHWEISMÖGLICHKEITEN</h2>
<div class="subsection" style="margin-left:20px;">
<p style="margin:0 0 6px;">7.1. Der Auftragnehmer weist dem Auftraggeber die Einhaltung der in diesem Vertrag niedergelegten Pflichten mit geeigneten Mitteln nach.</p>
<p style="margin:0 0 6px;">7.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>
<!-- MOVED UP: Sections 8 & 9 and signatures (formerly PAGE 4) -->
<div class="section" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">8. SUBUNTERNEHMER (WEITERE AUFTRAGSVERARBEITER)</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">8.1. Der Einsatz von Subunternehmern als weitere Auftragsverarbeiter ist nur zulässig, wenn der Auftraggeber vorher zugestimmt hat.</p>
<p style="margin:0 0 6px;">8.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 style="margin:0 0 6px;">8.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" style="margin-bottom:10px;">
<h2 style="margin:14px 0 6px;">9. INFORMATIONSPFLICHTEN, SCHRIFTFORMKLAUSEL, RECHTSWAHL</h2>
<div class="subsection" style="margin-left:16px;">
<p style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.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 style="margin:0 0 6px;">9.4. Es gilt das auf dem Hauptvertrag anwendbare Recht sowie Gerichtsstand.</p>
</div>
</div>
<div class="signatures" style="display:flex;gap:30px;margin-top:38px;">
<div class="signature" style="flex:1;text-align:center;">
<p style="margin:0 0 6px;">Für PROFIT PLANET (Auftraggeber)</p>
<!-- CHANGED: wrap stamp + date in a flex column with spacing to avoid overlap -->
<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 class="sig-date" 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" style="flex:1;text-align:center;">
<p style="margin:0 0 6px;">Für den VP (Auftragnehmer)</p>
<div class="sig-block" style="display:flex;flex-direction:column;align-items:center;gap:4px;min-height:110px;">
<span style="display:block;max-width:100%;max-height:80px;">{{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>

View File

@ -22,6 +22,8 @@ const GUEST_ALLOWED_PREFIXES = [
'/refresh', '/refresh',
'/coffee/active', '/coffee/active',
'/tax/vat-rates', '/tax/vat-rates',
'/send-verification-email',
'/verify-email-code',
]; ];
function guestRestriction(req, res, next) { function guestRestriction(req, res, next) {
@ -31,10 +33,16 @@ function guestRestriction(req, res, next) {
} }
const urlPath = req.originalUrl.split('?')[0]; const urlPath = req.originalUrl.split('?')[0];
// Strip /api prefix if present (routes are mounted at /api but prefixes listed without it)
const normalizedPath = urlPath.startsWith('/api/') ? urlPath.slice(4) : urlPath;
const isAllowed = GUEST_ALLOWED_PREFIXES.some((prefix) => urlPath.startsWith(prefix)); const isAllowed = GUEST_ALLOWED_PREFIXES.some((prefix) => normalizedPath.startsWith(prefix));
if (isAllowed) { // Allow guests to fetch their own permissions
const userId = user.userId || user.id;
const isOwnPermissions = userId && normalizedPath === `/users/${userId}/permissions`;
if (isAllowed || isOwnPermissions) {
return next(); return next();
} }

View File

@ -24,6 +24,21 @@ class Abonemment {
this.referred_by = row.referred_by; // NEW this.referred_by = row.referred_by; // NEW
this.purchaser_user_id = row.purchaser_user_id ?? null; // NEW this.purchaser_user_id = row.purchaser_user_id ?? null; // NEW
this.user_id = row.user_id ?? null; // NEW: map owner user_id this.user_id = row.user_id ?? null; // NEW: map owner user_id
this.contract_number = row.contract_number ?? null;
this.contract_storage_key = row.contract_storage_key ?? null;
// Contact / invoice fields
this.phone = row.phone ?? null;
this.recipient_name = row.recipient_name ?? null;
this.recipient_address = row.recipient_address ?? null;
this.payment_method = row.payment_method ?? null;
this.invoice_by_email = !!row.invoice_by_email;
this.invoice_same_as_shipping = row.invoice_same_as_shipping !== undefined ? !!row.invoice_same_as_shipping : true;
this.invoice_full_name = row.invoice_full_name ?? null;
this.invoice_street = row.invoice_street ?? null;
this.invoice_postal_code = row.invoice_postal_code ?? null;
this.invoice_city = row.invoice_city ?? null;
this.invoice_phone = row.invoice_phone ?? null;
this.invoice_email = row.invoice_email ?? null;
this.created_at = row.created_at; this.created_at = row.created_at;
this.updated_at = row.updated_at; this.updated_at = row.updated_at;
} }

226
package-lock.json generated
View File

@ -22,6 +22,7 @@
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"multer": "^2.0.2", "multer": "^2.0.2",
"mysql2": "^3.17.2", "mysql2": "^3.17.2",
"node-cron": "^4.2.1",
"nodemailer": "^8.0.1", "nodemailer": "^8.0.1",
"pdfkit": "^0.17.2", "pdfkit": "^0.17.2",
"pidusage": "^4.0.1", "pidusage": "^4.0.1",
@ -995,13 +996,13 @@
} }
}, },
"node_modules/@aws-sdk/xml-builder": { "node_modules/@aws-sdk/xml-builder": {
"version": "3.972.5", "version": "3.972.11",
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.5.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz",
"integrity": "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA==", "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@smithy/types": "^4.12.0", "@smithy/types": "^4.13.1",
"fast-xml-parser": "5.3.6", "fast-xml-parser": "5.4.1",
"tslib": "^2.6.2" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
@ -1598,9 +1599,9 @@
} }
}, },
"node_modules/@smithy/types": { "node_modules/@smithy/types": {
"version": "4.12.0", "version": "4.13.1",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz",
"integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "^2.6.2" "tslib": "^2.6.2"
@ -1868,9 +1869,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.3.0", "version": "25.5.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -2065,11 +2066,10 @@
} }
}, },
"node_modules/bare-fs": { "node_modules/bare-fs": {
"version": "4.5.4", "version": "4.5.5",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz", "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz",
"integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==", "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"dependencies": { "dependencies": {
"bare-events": "^2.5.4", "bare-events": "^2.5.4",
"bare-path": "^3.0.0", "bare-path": "^3.0.0",
@ -2090,11 +2090,10 @@
} }
}, },
"node_modules/bare-os": { "node_modules/bare-os": {
"version": "3.6.2", "version": "3.8.0",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz",
"integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"engines": { "engines": {
"bare": ">=1.14.0" "bare": ">=1.14.0"
} }
@ -2104,17 +2103,15 @@
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"dependencies": { "dependencies": {
"bare-os": "^3.0.1" "bare-os": "^3.0.1"
} }
}, },
"node_modules/bare-stream": { "node_modules/bare-stream": {
"version": "2.8.0", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz", "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.1.tgz",
"integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", "integrity": "sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"dependencies": { "dependencies": {
"streamx": "^2.21.0", "streamx": "^2.21.0",
"teex": "^1.0.1" "teex": "^1.0.1"
@ -2137,7 +2134,6 @@
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"dependencies": { "dependencies": {
"bare-path": "^3.0.0" "bare-path": "^3.0.0"
} }
@ -2163,9 +2159,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/basic-ftp": { "node_modules/basic-ftp": {
"version": "5.1.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
"integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==", "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
@ -2645,9 +2641,9 @@
} }
}, },
"node_modules/devtools-protocol": { "node_modules/devtools-protocol": {
"version": "0.0.1566079", "version": "0.0.1581282",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
"integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/dfa": { "node_modules/dfa": {
@ -2959,10 +2955,10 @@
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-xml-parser": { "node_modules/fast-xml-builder": {
"version": "5.3.7", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.7.tgz", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.3.tgz",
"integrity": "sha512-JzVLro9NQv92pOM/jTCR6mHlJh2FGwtomH8ZQjhFj/R29P2Fnj38OgPJVtcvYw6SuKClhgYuwUZf5b3rd8u2mA==", "integrity": "sha512-1o60KoFw2+LWKQu3IdcfcFlGTW4dpqEWmjhYec6H82AYZU2TVBXep6tMl8Z1Y+wM+ZrzCwe3BZ9Vyd9N2rIvmg==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -2971,21 +2967,29 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"path-expression-matcher": "^1.1.3"
}
},
"node_modules/fast-xml-parser": {
"version": "5.5.5",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.5.tgz",
"integrity": "sha512-NLY+V5NNbdmiEszx9n14mZBseJTC50bRq1VHsaxOmR72JDuZt+5J1Co+dC/4JPnyq+WrIHNM69r0sqf7BMb3Mg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"fast-xml-builder": "^1.1.3",
"path-expression-matcher": "^1.1.3",
"strnum": "^2.1.2" "strnum": "^2.1.2"
}, },
"bin": { "bin": {
"fxparser": "src/cli/cli.js" "fxparser": "src/cli/cli.js"
} }
}, },
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fecha": { "node_modules/fecha": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
@ -3694,9 +3698,9 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "10.2.2", "version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
@ -3709,33 +3713,12 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mitt": { "node_modules/mitt": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/moment": { "node_modules/moment": {
"version": "2.30.1", "version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
@ -3752,21 +3735,22 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/multer": { "node_modules/multer": {
"version": "2.0.2", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"append-field": "^1.0.0", "append-field": "^1.0.0",
"busboy": "^1.6.0", "busboy": "^1.6.0",
"concat-stream": "^2.0.0", "concat-stream": "^2.0.0",
"mkdirp": "^0.5.6", "type-is": "^1.6.18"
"object-assign": "^4.1.1",
"type-is": "^1.6.18",
"xtend": "^4.0.2"
}, },
"engines": { "engines": {
"node": ">= 10.16.0" "node": ">= 10.16.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/multer/node_modules/media-typer": { "node_modules/multer/node_modules/media-typer": {
@ -3870,6 +3854,15 @@
"node": "^18 || ^20 || >= 21" "node": "^18 || ^20 || >= 21"
} }
}, },
"node_modules/node-cron": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
"integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
"license": "ISC",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/node-gyp-build": { "node_modules/node-gyp-build": {
"version": "4.8.4", "version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
@ -4066,6 +4059,21 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/path-expression-matcher": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz",
"integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/path-key": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -4195,9 +4203,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/pump": { "node_modules/pump": {
"version": "3.0.3", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"end-of-stream": "^1.1.0", "end-of-stream": "^1.1.0",
@ -4205,18 +4213,18 @@
} }
}, },
"node_modules/puppeteer": { "node_modules/puppeteer": {
"version": "24.37.5", "version": "24.39.1",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.37.5.tgz", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.39.1.tgz",
"integrity": "sha512-3PAOIQLceyEmn1Fi76GkGO2EVxztv5OtdlB1m8hMUZL3f8KDHnlvXbvCXv+Ls7KzF1R0KdKBqLuT/Hhrok12hQ==", "integrity": "sha512-68Zc9QpcVvfxp2C+3UL88TyUogEAn5tSylXidbEuEXvhiqK1+v65zeBU5ubinAgEHMGr3dcSYqvYrGtdzsPI3w==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@puppeteer/browsers": "2.13.0", "@puppeteer/browsers": "2.13.0",
"chromium-bidi": "14.0.0", "chromium-bidi": "14.0.0",
"cosmiconfig": "^9.0.0", "cosmiconfig": "^9.0.0",
"devtools-protocol": "0.0.1566079", "devtools-protocol": "0.0.1581282",
"puppeteer-core": "24.37.5", "puppeteer-core": "24.39.1",
"typed-query-selector": "^2.12.0" "typed-query-selector": "^2.12.1"
}, },
"bin": { "bin": {
"puppeteer": "lib/cjs/puppeteer/node/cli.js" "puppeteer": "lib/cjs/puppeteer/node/cli.js"
@ -4226,16 +4234,16 @@
} }
}, },
"node_modules/puppeteer-core": { "node_modules/puppeteer-core": {
"version": "24.37.5", "version": "24.39.1",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.37.5.tgz", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.39.1.tgz",
"integrity": "sha512-ybL7iE78YPN4T6J+sPLO7r0lSByp/0NN6PvfBEql219cOnttoTFzCWKiBOjstXSqi/OKpwae623DWAsL7cn2MQ==", "integrity": "sha512-AMqQIKoEhPS6CilDzw0Gd1brLri3emkC+1N2J6ZCCuY1Cglo56M63S0jOeBZDQlemOiRd686MYVMl9ELJBzN3A==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@puppeteer/browsers": "2.13.0", "@puppeteer/browsers": "2.13.0",
"chromium-bidi": "14.0.0", "chromium-bidi": "14.0.0",
"debug": "^4.4.3", "debug": "^4.4.3",
"devtools-protocol": "0.0.1566079", "devtools-protocol": "0.0.1581282",
"typed-query-selector": "^2.12.0", "typed-query-selector": "^2.12.1",
"webdriver-bidi-protocol": "0.4.1", "webdriver-bidi-protocol": "0.4.1",
"ws": "^8.19.0" "ws": "^8.19.0"
}, },
@ -4714,9 +4722,9 @@
} }
}, },
"node_modules/tar-fs": { "node_modules/tar-fs": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"pump": "^3.0.0", "pump": "^3.0.0",
@ -4728,12 +4736,13 @@
} }
}, },
"node_modules/tar-stream": { "node_modules/tar-stream": {
"version": "3.1.7", "version": "3.1.8",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"b4a": "^1.6.4", "b4a": "^1.6.4",
"bare-fs": "^4.5.5",
"fast-fifo": "^1.2.0", "fast-fifo": "^1.2.0",
"streamx": "^2.15.0" "streamx": "^2.15.0"
} }
@ -4743,7 +4752,6 @@
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"streamx": "^2.12.5" "streamx": "^2.12.5"
} }
@ -4831,9 +4839,9 @@
} }
}, },
"node_modules/typed-query-selector": { "node_modules/typed-query-selector": {
"version": "2.12.0", "version": "2.12.1",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz",
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/typedarray": { "node_modules/typedarray": {
@ -5044,15 +5052,6 @@
} }
} }
}, },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@ -5090,13 +5089,16 @@
} }
}, },
"node_modules/yauzl": { "node_modules/yauzl": {
"version": "2.10.0", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.1.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "integrity": "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"buffer-crc32": "~0.2.3", "buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0" "pend": "~1.2.0"
},
"engines": {
"node": ">=12"
} }
}, },
"node_modules/zod": { "node_modules/zod": {

View File

@ -19,9 +19,10 @@
}, },
"overrides": { "overrides": {
"@aws-sdk/xml-builder": { "@aws-sdk/xml-builder": {
"fast-xml-parser": "^5.3.4", "fast-xml-parser": "^5.3.4",
"ajv": "8.18.0" "ajv": "8.18.0"
} },
"yauzl": "^3.2.1"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.992.0", "@aws-sdk/client-s3": "^3.992.0",
@ -37,6 +38,7 @@
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"multer": "^2.0.2", "multer": "^2.0.2",
"mysql2": "^3.17.2", "mysql2": "^3.17.2",
"node-cron": "^4.2.1",
"nodemailer": "^8.0.1", "nodemailer": "^8.0.1",
"pdfkit": "^0.17.2", "pdfkit": "^0.17.2",
"pidusage": "^4.0.1", "pidusage": "^4.0.1",

View File

@ -74,6 +74,28 @@ class AbonemmentRepository {
vals.push(snapshot.purchaser_user_id ?? null); vals.push(snapshot.purchaser_user_id ?? null);
} }
// New contract / contact / invoice columns
const optionalCols = [
['phone', snapshot.phone || null],
['recipient_name', snapshot.recipient_name || null],
['recipient_address', snapshot.recipient_address || null],
['payment_method', snapshot.payment_method || null],
['invoice_by_email', snapshot.invoice_by_email ? 1 : 0],
['invoice_same_as_shipping', snapshot.invoice_same_as_shipping !== false ? 1 : 0],
['invoice_full_name', snapshot.invoice_full_name || null],
['invoice_street', snapshot.invoice_street || null],
['invoice_postal_code', snapshot.invoice_postal_code || null],
['invoice_city', snapshot.invoice_city || null],
['invoice_phone', snapshot.invoice_phone || null],
['invoice_email', snapshot.invoice_email || null],
];
for (const [col, val] of optionalCols) {
if (await this.hasColumn(col)) {
cols.push(col);
vals.push(val);
}
}
const placeholders = cols.map(() => '?').join(', '); const placeholders = cols.map(() => '?').join(', ');
console.log('[CREATE ABONEMENT] Final columns:', cols); console.log('[CREATE ABONEMENT] Final columns:', cols);
console.log('[CREATE ABONEMENT] Final values preview:', { console.log('[CREATE ABONEMENT] Final values preview:', {
@ -182,6 +204,29 @@ class AbonemmentRepository {
return this.getAbonementById(id); return this.getAbonementById(id);
} }
async updateContractInfo(id, { contractNumber = null, storageKey = null } = {}) {
const sets = [];
const params = [];
if (await this.hasColumn('contract_number') && contractNumber) {
sets.push('contract_number = ?');
params.push(String(contractNumber));
}
if (await this.hasColumn('contract_storage_key') && storageKey) {
sets.push('contract_storage_key = ?');
params.push(String(storageKey));
}
if (!sets.length) return;
sets.push('updated_at = NOW()');
await pool.query(
`UPDATE coffee_abonements SET ${sets.join(', ')} WHERE id = ?`,
[...params, id],
);
}
async appendHistory(abonementId, eventType, actorUserId, details = {}, eventAt = new Date()) { async appendHistory(abonementId, eventType, actorUserId, details = {}, eventAt = new Date()) {
await pool.query( await pool.query(
`INSERT INTO coffee_abonement_history `INSERT INTO coffee_abonement_history
@ -304,6 +349,15 @@ class AbonemmentRepository {
return rows.map((r) => new Abonemment(r)); return rows.map((r) => new Abonemment(r));
} }
async listPausedAutoRenew() {
const [rows] = await pool.query(
`SELECT * FROM coffee_abonements
WHERE status = 'paused' AND is_auto_renew = 1
ORDER BY updated_at ASC`,
);
return rows.map((r) => new Abonemment(r));
}
async listActiveByProduct(productId) { async listActiveByProduct(productId) {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT * FROM coffee_abonements WHERE coffee_table_id = ? AND status = 'active'`, `SELECT * FROM coffee_abonements WHERE coffee_table_id = ? AND status = 'active'`,

View File

@ -207,7 +207,7 @@ class InvoiceRepository {
} }
async updateStatus(invoiceId, newStatus) { async updateStatus(invoiceId, newStatus) {
const allowed = ['draft', 'issued', 'paid', 'overdue', 'canceled']; const allowed = ['draft', 'issued', 'paid', 'canceled'];
if (!allowed.includes(newStatus)) { if (!allowed.includes(newStatus)) {
throw new Error(`Invalid status '${newStatus}'. Allowed: ${allowed.join(', ')}`); throw new Error(`Invalid status '${newStatus}'. Allowed: ${allowed.join(', ')}`);
} }

View File

@ -6,16 +6,42 @@ class CompanySettingsRepository {
return rows[0] || null; return rows[0] || null;
} }
async update({ company_name, company_street, company_postal_city, company_country }) { async update({
company_name,
company_street,
company_postal_city,
company_country,
qr_code_60_base64,
qr_code_120_base64,
} = {}) {
const current = await this.get();
const next = {
company_name: company_name !== undefined ? company_name : (current?.company_name ?? ''),
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),
};
await pool.query( await pool.query(
`INSERT INTO company_settings (id, company_name, company_street, company_postal_city, company_country) `INSERT INTO company_settings (id, company_name, company_street, company_postal_city, company_country, qr_code_60_base64, qr_code_120_base64)
VALUES (1, ?, ?, ?, ?) VALUES (1, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
company_name = VALUES(company_name), company_name = VALUES(company_name),
company_street = VALUES(company_street), company_street = VALUES(company_street),
company_postal_city = VALUES(company_postal_city), company_postal_city = VALUES(company_postal_city),
company_country = VALUES(company_country)`, company_country = VALUES(company_country),
[company_name || '', company_street || '', company_postal_city || '', company_country || ''] qr_code_60_base64 = VALUES(qr_code_60_base64),
qr_code_120_base64 = VALUES(qr_code_120_base64)`,
[
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,
]
); );
return this.get(); return this.get();
} }

View File

@ -0,0 +1,142 @@
const pool = require('../../database/database');
class DashboardPlatformsRepository {
async _hasTable() {
const [rows] = await pool.query(
`SELECT 1
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'dashboard_plattforms'
LIMIT 1`
);
return Array.isArray(rows) && rows.length > 0;
}
_mapRow(row) {
if (!row) return null;
return {
id: row.id,
title: row.title,
description: row.description ?? '',
href: row.href,
icon: row.icon ?? '',
color: row.color ?? '',
state: row.state !== 0 && row.state !== false,
disabled: row.disabled === 1 || row.disabled === true,
disabledText: row.disabled_text ?? null,
sortOrder: Number.isFinite(Number(row.sort_order)) ? Number(row.sort_order) : 0,
};
}
async list() {
const has = await this._hasTable();
if (!has) return [];
const [rows] = await pool.query(
`SELECT id, title, description, href, icon, color, state, disabled, disabled_text, sort_order
FROM dashboard_plattforms
ORDER BY sort_order ASC, title ASC`
);
return (rows || []).map(r => this._mapRow(r));
}
async listPublic() {
const has = await this._hasTable();
if (!has) return [];
const [rows] = await pool.query(
`SELECT id, title, description, href, icon, color, state, disabled, disabled_text, sort_order
FROM dashboard_plattforms
WHERE state = 1
ORDER BY sort_order ASC, title ASC`
);
return (rows || []).map(r => this._mapRow(r));
}
async getById(id) {
const has = await this._hasTable();
if (!has) return null;
const [rows] = await pool.query(
`SELECT id, title, description, href, icon, color, state, disabled, disabled_text, sort_order
FROM dashboard_plattforms
WHERE id = ?
LIMIT 1`,
[id]
);
return this._mapRow(rows?.[0]);
}
async create(platform) {
const has = await this._hasTable();
if (!has) throw new Error('dashboard_plattforms table missing');
await pool.query(
`INSERT INTO dashboard_plattforms
(id, title, description, href, icon, color, state, disabled, disabled_text, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
platform.id,
platform.title,
platform.description ?? '',
platform.href,
platform.icon ?? '',
platform.color ?? '',
platform.state ? 1 : 0,
platform.disabled ? 1 : 0,
platform.disabledText ?? null,
Number.isFinite(Number(platform.sortOrder)) ? Number(platform.sortOrder) : 0,
]
);
return this.getById(platform.id);
}
async update(id, fields) {
const has = await this._hasTable();
if (!has) throw new Error('dashboard_plattforms table missing');
const [result] = await pool.query(
`UPDATE dashboard_plattforms
SET
title = ?,
description = ?,
href = ?,
icon = ?,
color = ?,
disabled = ?,
disabled_text = ?,
sort_order = ?
WHERE id = ?`,
[
fields.title,
fields.description ?? '',
fields.href,
fields.icon ?? '',
fields.color ?? '',
fields.disabled ? 1 : 0,
fields.disabledText ?? null,
Number.isFinite(Number(fields.sortOrder)) ? Number(fields.sortOrder) : 0,
id,
]
);
if (!result?.affectedRows) return null;
return this.getById(id);
}
async setState(id, state) {
const has = await this._hasTable();
if (!has) throw new Error('dashboard_plattforms table missing');
const [result] = await pool.query(
`UPDATE dashboard_plattforms SET state = ? WHERE id = ?`,
[state ? 1 : 0, id]
);
if (!result?.affectedRows) return null;
return this.getById(id);
}
}
module.exports = DashboardPlatformsRepository;

View File

@ -22,9 +22,10 @@ class DocumentTemplateRepository {
const lang = String(data.lang); const lang = String(data.lang);
const allowedUserTypes = new Set(['personal', 'company', 'both']); const allowedUserTypes = new Set(['personal', 'company', 'both']);
const user_type = allowedUserTypes.has(data.user_type || data.userType) ? (data.user_type || data.userType) : 'both'; const user_type = allowedUserTypes.has(data.user_type || data.userType) ? (data.user_type || data.userType) : 'both';
const allowedContractTypes = new Set(['contract', 'gdpr']); const allowedContractTypes = new Set(['contract', 'gdpr', 'abo']);
const normalizeContractType = (value) => (value === undefined || value === null) ? value : String(value).trim().toLowerCase();
const contract_type = type === 'contract' const contract_type = type === 'contract'
? (allowedContractTypes.has(data.contract_type || data.contractType) ? (data.contract_type || data.contractType) : null) ? (allowedContractTypes.has(normalizeContractType(data.contract_type || data.contractType)) ? normalizeContractType(data.contract_type || data.contractType) : null)
: null; : null;
const finalContractType = type === 'contract' ? (contract_type || 'contract') : null; const finalContractType = type === 'contract' ? (contract_type || 'contract') : null;
@ -152,7 +153,10 @@ class DocumentTemplateRepository {
// - overlapping user_type (personal/company/both) // - overlapping user_type (personal/company/both)
async deactivateOtherActiveContracts({ excludeId, contract_type, lang, user_type }, conn) { async deactivateOtherActiveContracts({ excludeId, contract_type, lang, user_type }, conn) {
logger.info('DocumentTemplateRepository.deactivateOtherActiveContracts:start', { excludeId, contract_type, lang, user_type }); logger.info('DocumentTemplateRepository.deactivateOtherActiveContracts:start', { excludeId, contract_type, lang, user_type });
const safeContractType = (contract_type === 'gdpr' || contract_type === 'contract') ? contract_type : 'contract'; const normalizedContractType = (contract_type === undefined || contract_type === null) ? '' : String(contract_type).trim().toLowerCase();
const safeContractType = (normalizedContractType === 'gdpr' || normalizedContractType === 'contract' || normalizedContractType === 'abo')
? normalizedContractType
: 'contract';
const safeLang = (lang === 'en' || lang === 'de') ? lang : 'en'; const safeLang = (lang === 'en' || lang === 'de') ? lang : 'en';
const safeUserType = (user_type === 'personal' || user_type === 'company' || user_type === 'both') ? user_type : 'both'; const safeUserType = (user_type === 'personal' || user_type === 'company' || user_type === 'both') ? user_type : 'both';

View File

@ -28,6 +28,8 @@ const NewsController = require('../controller/news/NewsController');
const InvoiceController = require('../controller/invoice/InvoiceController'); // NEW const InvoiceController = require('../controller/invoice/InvoiceController'); // NEW
const DevManagementController = require('../controller/dev/DevManagementController'); const DevManagementController = require('../controller/dev/DevManagementController');
const CompanySettingsController = require('../controller/admin/CompanySettingsController'); const CompanySettingsController = require('../controller/admin/CompanySettingsController');
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
const ShippingFeesController = require('../controller/admin/ShippingFeesController');
// small helpers copied from original files // small helpers copied from original files
@ -64,6 +66,14 @@ router.get('/admin/server-status', authMiddleware, adminOnly, ServerStatusContro
router.get('/admin/dev/exoscale/folder-structure-issues', authMiddleware, adminOnly, DevManagementController.listFolderStructureIssues); router.get('/admin/dev/exoscale/folder-structure-issues', authMiddleware, adminOnly, DevManagementController.listFolderStructureIssues);
router.get('/admin/dev/exoscale/loose-files', authMiddleware, adminOnly, DevManagementController.listLooseFiles); router.get('/admin/dev/exoscale/loose-files', authMiddleware, adminOnly, DevManagementController.listLooseFiles);
router.get('/admin/dev/exoscale/ghost-directories', authMiddleware, adminOnly, DevManagementController.listGhostDirectories); router.get('/admin/dev/exoscale/ghost-directories', authMiddleware, adminOnly, DevManagementController.listGhostDirectories);
// Admin: dashboard platforms
router.get('/admin/dashboard-platforms', authMiddleware, adminOnly, DashboardPlatformsController.list);
// Public: dashboard platforms (active only)
router.get('/dashboard-platforms', DashboardPlatformsController.listPublic);
// Public: shipping fees (60/120)
router.get('/shipping-fees', ShippingFeesController.listPublic);
// Contract preview for admin: latest active by user type // Contract preview for admin: latest active by user type
router.get('/admin/contracts/:id/preview', authMiddleware, adminOnly, DocumentTemplateController.previewLatestForUser); router.get('/admin/contracts/:id/preview', authMiddleware, adminOnly, DocumentTemplateController.previewLatestForUser);
// Admin: list all contract documents for a user // Admin: list all contract documents for a user
@ -100,6 +110,9 @@ router.get('/contracts/company', authMiddleware, (req, res) => {
// User: preview latest active contract (HTML) for authenticated user // User: preview latest active contract (HTML) for authenticated user
router.get('/contracts/preview/latest', authMiddleware, DocumentTemplateController.previewLatestForMe); router.get('/contracts/preview/latest', authMiddleware, DocumentTemplateController.previewLatestForMe);
// User: preview latest active ABO contract (HTML) for authenticated user
router.get('/contracts/abo/active', authMiddleware, DocumentTemplateController.previewLatestAboForMe);
// documentTemplates.js GETs // documentTemplates.js GETs
router.get('/document-templates', authMiddleware, DocumentTemplateController.listTemplates); router.get('/document-templates', authMiddleware, DocumentTemplateController.listTemplates);
router.get('/document-templates/:id', authMiddleware, DocumentTemplateController.getTemplate); router.get('/document-templates/:id', authMiddleware, DocumentTemplateController.getTemplate);

View File

@ -14,6 +14,7 @@ const AffiliateController = require('../controller/affiliate/AffiliateController
const NewsController = require('../controller/news/NewsController'); const NewsController = require('../controller/news/NewsController');
const AbonemmentController = require('../controller/abonemments/AbonemmentController'); const AbonemmentController = require('../controller/abonemments/AbonemmentController');
const InvoiceController = require('../controller/invoice/InvoiceController'); const InvoiceController = require('../controller/invoice/InvoiceController');
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
const multer = require('multer'); const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() }); const upload = multer({ storage: multer.memoryStorage() });
@ -60,6 +61,9 @@ router.patch('/admin/news/:id/status', authMiddleware, adminOnly, NewsController
// Admin: update invoice status // Admin: update invoice status
router.patch('/admin/invoices/:id/status', authMiddleware, adminOnly, InvoiceController.updateStatus); router.patch('/admin/invoices/:id/status', authMiddleware, adminOnly, InvoiceController.updateStatus);
// Admin: dashboard platforms state
router.patch('/admin/dashboard-platforms/:id/state', authMiddleware, adminOnly, DashboardPlatformsController.setState);
// Personal profile (self-service) - no admin guard // Personal profile (self-service) - no admin guard
router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic); router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic);
router.patch('/profile/personal/bank', authMiddleware, PersonalProfileController.updateBank); router.patch('/profile/personal/bank', authMiddleware, PersonalProfileController.updateBank);

View File

@ -31,6 +31,7 @@ const AbonemmentController = require('../controller/abonemments/AbonemmentContro
const NewsController = require('../controller/news/NewsController'); const NewsController = require('../controller/news/NewsController');
const InvoiceController = require('../controller/invoice/InvoiceController'); // NEW const InvoiceController = require('../controller/invoice/InvoiceController'); // NEW
const DevManagementController = require('../controller/dev/DevManagementController'); const DevManagementController = require('../controller/dev/DevManagementController');
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
const multer = require('multer'); const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() }); const upload = multer({ storage: multer.memoryStorage() });
@ -163,6 +164,9 @@ router.post('/admin/affiliates', authMiddleware, adminOnly, upload.single('logo'
// NEW: Admin create news with image upload // NEW: Admin create news with image upload
router.post('/admin/news', authMiddleware, adminOnly, upload.single('image'), NewsController.create); router.post('/admin/news', authMiddleware, adminOnly, upload.single('image'), NewsController.create);
// NEW: Admin dashboard platforms
router.post('/admin/dashboard-platforms', authMiddleware, adminOnly, DashboardPlatformsController.create);
// NEW: Dev Management SQL dump import (admin + super_admin) // NEW: Dev Management SQL dump import (admin + super_admin)
router.post('/admin/dev/sql', authMiddleware, adminOnly, upload.single('file'), DevManagementController.importSqlDump); router.post('/admin/dev/sql', authMiddleware, adminOnly, upload.single('file'), DevManagementController.importSqlDump);
// NEW: Dev Management Exoscale folder structure + loose file actions (admin + super_admin) // NEW: Dev Management Exoscale folder structure + loose file actions (admin + super_admin)

View File

@ -7,6 +7,8 @@ const AdminUserController = require('../controller/admin/AdminUserController');
const DocumentTemplateController = require('../controller/documentTemplate/DocumentTemplateController'); const DocumentTemplateController = require('../controller/documentTemplate/DocumentTemplateController');
const CoffeeController = require('../controller/admin/CoffeeController'); const CoffeeController = require('../controller/admin/CoffeeController');
const CompanySettingsController = require('../controller/admin/CompanySettingsController'); const CompanySettingsController = require('../controller/admin/CompanySettingsController');
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
const ShippingFeesController = require('../controller/admin/ShippingFeesController');
const multer = require('multer'); const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() }); const upload = multer({ storage: multer.memoryStorage() });
@ -21,4 +23,10 @@ router.put('/admin/coffee/:id', authMiddleware, adminOnly, upload.single('pictur
// Admin: update company settings (invoice address etc.) // Admin: update company settings (invoice address etc.)
router.put('/admin/company-settings', authMiddleware, adminOnly, CompanySettingsController.update); router.put('/admin/company-settings', authMiddleware, adminOnly, CompanySettingsController.update);
// Admin: update dashboard platform
router.put('/admin/dashboard-platforms/:id', authMiddleware, adminOnly, DashboardPlatformsController.update);
// Admin: update shipping fee for a piece count (60/120)
router.put('/admin/shipping-fees/:pieceCount', authMiddleware, adminOnly, ShippingFeesController.updatePrice);
module.exports = router; module.exports = router;

View File

@ -3,7 +3,7 @@ const UnitOfWork = require('../database/UnitOfWork');
const argon2 = require('argon2'); const argon2 = require('argon2');
async function createAdminUser() { async function createAdminUser() {
return;
// const adminEmail = process.env.ADMIN_EMAIL || 'office@profit-planet.com'; // const adminEmail = process.env.ADMIN_EMAIL || 'office@profit-planet.com';
const adminEmail = process.env.ADMIN_EMAIL || 'alexander.ibrahim.ai@gmail.com'; const adminEmail = process.env.ADMIN_EMAIL || 'alexander.ibrahim.ai@gmail.com';
// const adminEmail = process.env.ADMIN_EMAIL || 'loki.aahi@gmail.com'; // const adminEmail = process.env.ADMIN_EMAIL || 'loki.aahi@gmail.com';

View File

@ -0,0 +1,98 @@
const UnitOfWork = require('../database/UnitOfWork');
const argon2 = require('argon2');
async function createGuestUser() {
// Edit these values directly in code (no env vars)
const guestEmail = 'dummy-guest@profitplanet.local';
const guestPassword = 'dummyPass!1234';
const firstName = 'Guest';
const lastName = 'User';
const uow = new UnitOfWork();
await uow.start();
try {
const [existing] = await uow.connection.query(
`SELECT id FROM users WHERE email = ? LIMIT 1`,
[guestEmail]
);
const hashed = await argon2.hash(guestPassword);
let userId;
if (existing.length) {
userId = existing[0].id;
await uow.connection.query(
`UPDATE users SET password = ?, user_type = 'personal', role = 'guest' WHERE id = ?`,
[hashed, userId]
);
// Ensure personal_profile exists
const [profileExists] = await uow.connection.query(
`SELECT 1 FROM personal_profiles WHERE user_id = ? LIMIT 1`, [userId]
);
if (!profileExists.length) {
await uow.connection.query(
`INSERT INTO personal_profiles (user_id, first_name, last_name) VALUES (?, ?, ?)`,
[userId, firstName, lastName]
);
}
// Fix status: active, admin verified, email NOT verified (must verify on first login)
await uow.connection.query(
`UPDATE user_status SET status = 'active', is_admin_verified = 1, email_verified = 0,
profile_completed = 1, documents_uploaded = 1, contract_signed = 1, registration_completed = 1
WHERE user_id = ?`,
[userId]
);
console.log('✅ Guest user updated (password, role, status fixed)');
} else {
// Create base user record — user_type='personal', role='guest'
// (same pattern as GuestUserService)
const [userRes] = await uow.connection.query(
`INSERT INTO users (email, password, user_type, role, created_at) VALUES (?, ?, 'personal', 'guest', NOW())`,
[guestEmail, hashed]
);
userId = userRes.insertId;
// Create personal profile (required for login to return name fields)
await uow.connection.query(
`INSERT INTO personal_profiles (user_id, first_name, last_name) VALUES (?, ?, ?)`,
[userId, firstName, lastName]
);
// Initialize user status — active, admin verified
// email_verified = FALSE so the guest must verify email on first login
await uow.connection.query(
`INSERT INTO user_status (user_id, status, is_admin_verified, admin_verified_at,
email_verified, profile_completed, documents_uploaded, contract_signed, registration_completed)
VALUES (?, 'active', 1, NOW(), 0, 1, 1, 1, 1)`,
[userId]
);
// Initialize user settings
await uow.connection.query(
`INSERT INTO user_settings (user_id) VALUES (?)`,
[userId]
);
console.log('✅ Guest user created and initialized');
}
await uow.commit();
console.log(`📧 Email: ${guestEmail}`);
console.log(`🔑 Password: ${guestPassword}`);
console.log(`🆔 User ID: ${userId}`);
return { ok: true, userId, email: guestEmail };
} catch (error) {
await uow.rollback(error);
console.error('💥 Failed to create guest user:', error);
throw error;
}
}
module.exports = createGuestUser;
if (require.main === module) {
createGuestUser()
.then(() => process.exit(0))
.catch(() => process.exit(1));
}

View File

@ -0,0 +1,45 @@
/**
* Test script for the auto-renewal cron job.
*
* Usage:
* node scripts/testRenewalCron.js
*
* This will immediately run the full cron logic:
* - Renew if last invoice is 'paid' (or no invoice exists) generate new invoice + advance next_billing_at
* - Skip if last invoice is NOT paid just advance next_billing_at, no new invoice
* - Skip if gift-abo without registered user (user_id NULL) don't advance
*
* Test setup (run in MySQL/phpMyAdmin):
*
* -- 1. Make a subscription due for billing:
* UPDATE coffee_abonements SET next_billing_at = NOW() - INTERVAL 1 DAY WHERE id = <ID>;
*
* -- 2. To test "paid" flow: mark last invoice as paid:
* UPDATE invoices SET status = 'paid' WHERE source_type = 'subscription' AND source_id = <ABO_ID> ORDER BY id DESC LIMIT 1;
*
* -- 3. To test "unpaid" flow: leave invoice status as 'issued'
*
* -- 4. To test gift skip: set user_id to NULL:
* UPDATE coffee_abonements SET user_id = NULL WHERE id = <ID>;
*/
require('dotenv').config();
const RenewalCronService = require('../services/abonemments/RenewalCronService');
(async () => {
console.log('=== Renewal Cron Test ===');
console.log('Time:', new Date().toISOString());
console.log('Day of month:', new Date().getDate(), '(reactivation before 11th?', new Date().getDate() <= 10, ')');
console.log('');
const cron = new RenewalCronService();
try {
await cron.processDueRenewals();
console.log('');
console.log('=== Test complete ===');
} catch (err) {
console.error('Test failed:', err);
}
setTimeout(() => process.exit(0), 3000);
})();

View File

@ -13,10 +13,15 @@ const permissionsInit = require('./scripts/initPermissions');
const createAdminUser = require('./scripts/createAdminUser'); const createAdminUser = require('./scripts/createAdminUser');
const createCompanyUser = require('./scripts/createCompanyUser'); const createCompanyUser = require('./scripts/createCompanyUser');
const createPersonalUser = require('./scripts/createPersonalUser'); const createPersonalUser = require('./scripts/createPersonalUser');
const createGuestUser = require('./scripts/createGuestUser');
const RenewalCronService = require('./services/abonemments/RenewalCronService');
const app = express(); const app = express();
const PORT = process.env.PORT || 3001; const PORT = process.env.PORT || 3001;
// Increase body size limits for base64 payloads (e.g. QR code images)
const JSON_LIMIT = process.env.JSON_LIMIT || '2mb';
// CORS configuration // CORS configuration
const ALLOWED_ORIGINS = (process.env.CORS_ALLOWED_ORIGINS) const ALLOWED_ORIGINS = (process.env.CORS_ALLOWED_ORIGINS)
.split(',') .split(',')
@ -39,7 +44,8 @@ const corsOptions = {
app.use(cors(corsOptions)); app.use(cors(corsOptions));
// Middleware // Middleware
app.use(express.json()); app.use(express.json({ limit: JSON_LIMIT }));
app.use(express.urlencoded({ extended: true, limit: JSON_LIMIT }));
app.use(cookieParser()); app.use(cookieParser());
// -- Replace inline console logging with structured request logger -- // -- Replace inline console logging with structured request logger --
@ -183,6 +189,13 @@ async function startServer() {
// Create personal user // Create personal user
await createPersonalUser(); await createPersonalUser();
// Create guest user
await createGuestUser();
// Start automatic subscription renewal cron job
const renewalCron = new RenewalCronService();
renewalCron.start();
// Start the server // Start the server
app.listen(PORT, () => { app.listen(PORT, () => {
const host = process.env.HOST || 'localhost'; const host = process.env.HOST || 'localhost';

View File

@ -0,0 +1,379 @@
const fs = require('fs');
const path = require('path');
const puppeteer = require('puppeteer');
const { PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
const { s3: sharedExoscaleClient } = require('../../utils/exoscaleUploader');
const DocumentTemplateService = require('../template/DocumentTemplateService');
const MailService = require('../email/MailService');
const pool = require('../../database/database');
const { logger } = require('../../middleware/logger');
class AboContractService {
constructor() {
this.templatePath = path.join(__dirname, '..', '..', 'templates', 'abo', 'abo-contract-template-new.html');
}
/**
* Load the latest active abo contract template from the contract management system (DB + S3).
* Falls back to the local file if no active template is found.
*/
async _loadTemplate(userType = 'both') {
try {
const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, 'contract', 'abo');
if (latest?.storageKey) {
const command = new GetObjectCommand({
Bucket: process.env.EXOSCALE_BUCKET,
Key: latest.storageKey,
});
const fileObj = await sharedExoscaleClient.send(command);
if (fileObj.Body) {
const chunks = [];
for await (const chunk of fileObj.Body) {
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
}
const html = Buffer.concat(chunks).toString('utf-8');
if (html.trim()) {
logger.info('AboContractService:template_loaded_from_s3', { id: latest.id, storageKey: latest.storageKey });
return html;
}
}
}
} catch (e) {
logger.warn('AboContractService:s3_template_load_failed', { message: e?.message });
}
// Fallback: local file
return fs.readFileSync(this.templatePath, 'utf8');
}
_escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
_formatDateTime(d = new Date()) {
const pad = (n) => String(n).padStart(2, '0');
return `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
_sanitizeKeyPart(input, fallback) {
const raw = String(input ?? '').trim();
const safe = raw.replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 80);
return safe || fallback;
}
_renderTemplate(template, variables, { rawKeys = new Set() } = {}) {
if (!template) return '';
return template.replace(/{{\s*([\w]+)\s*}}/g, (_, key) => {
const value = variables[key];
if (value === undefined || value === null) return '';
if (rawKeys.has(key)) return String(value);
return this._escapeHtml(String(value));
});
}
_buildSelectedProductsHtml(abonement) {
const breakdown = Array.isArray(abonement?.pack_breakdown) ? abonement.pack_breakdown : [];
if (!breakdown.length) return '<p>-</p>';
const rows = breakdown.map((item) => {
const title = this._escapeHtml(item?.coffee_title || item?.title || 'Coffee');
const packs = Number(item?.packs ?? item?.quantity ?? 0);
return `<li>${title}${Number.isFinite(packs) ? packs : 0} pack(s)</li>`;
}).join('');
return `<ul>${rows}</ul>`;
}
_buildSignatureImgHtml(signatureDataUrl) {
const s = typeof signatureDataUrl === 'string' ? signatureDataUrl.trim() : '';
if (!s) return '';
// Basic safety: only accept data:image/... urls
if (!/^data:image\//i.test(s)) return '';
const escaped = this._escapeHtml(s);
return `<img alt="Signature" src="${escaped}" style="max-width:280px;max-height:120px;" />`;
}
async _renderPdfFromHtml(html) {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '16mm', right: '14mm', bottom: '16mm', left: '14mm' },
});
return Buffer.isBuffer(pdfBuffer) ? pdfBuffer : Buffer.from(pdfBuffer);
} finally {
await browser.close();
}
}
async generateUploadAndEmail({
abonement,
actorUser,
contractNumber,
signingCity,
signatureDataUrl,
isForSelf,
lang = 'en',
}) {
if (!abonement?.id) throw new Error('abonement is required');
// Load template from contract management system (DB + S3), fallback to local file
const userType = await this._resolveUserType(actorUser);
let template;
try {
template = await this._loadTemplate(userType);
} catch (e) {
logger.error('AboContractService:template_missing', { message: e?.message });
throw new Error('ABO contract template missing');
}
// --- Sequential contract number ---
let displayContractNumber = String(contractNumber || '').trim();
if (!displayContractNumber) {
displayContractNumber = await this._generateSequentialContractNumber();
}
const contractKeyPart = this._sanitizeKeyPart(displayContractNumber, `abo-${abonement.id}`);
let profitplanetSignature = '';
try {
const sig = await DocumentTemplateService.getProfitPlanetSignatureTag({ maxW: 300, maxH: 300 });
profitplanetSignature = sig?.tag || '';
logger.info('AboContractService:stamp_result', { reason: sig?.reason, hasTag: !!profitplanetSignature, tagLen: profitplanetSignature.length });
} catch (e) {
logger.warn('AboContractService:getProfitPlanetSignatureTag_failed', { message: e?.message });
}
// --- Determine user type from actorUser or DB ---
// (userType already resolved above for template loading)
const isCompany = userType === 'company';
// --- Company data (FN / ATU) ---
let fnNumber = '';
let atuNumber = '';
if (isCompany && actorUser?.id) {
const companyData = await this._loadCompanyData(actorUser.id);
fnNumber = companyData.registrationNumber || '';
atuNumber = companyData.atuNumber || '';
}
// --- Shipping & Invoice data ---
const fullName = `${abonement.first_name || ''} ${abonement.last_name || ''}`.trim();
const invoiceSame = abonement.invoice_same_as_shipping !== false;
// DEBUG: log abonement data to diagnose empty fields in PDF
logger.info('AboContractService:abonement_data', {
id: abonement.id,
first_name: abonement.first_name,
last_name: abonement.last_name,
email: abonement.email,
street: abonement.street,
postal_code: abonement.postal_code,
city: abonement.city,
phone: abonement.phone,
payment_method: abonement.payment_method,
invoice_by_email: abonement.invoice_by_email,
invoice_same_as_shipping: abonement.invoice_same_as_shipping,
recipient_name: abonement.recipient_name,
fullName,
invoiceSame,
});
const variables = {
// Meta
contractNumber: displayContractNumber,
currentDate: this._formatDateTime(new Date()),
// Empfänger (An die) — auto-fill from shipping data if no explicit recipient set
recipientName: abonement.recipient_name || fullName,
recipientAddress: abonement.recipient_address || `${abonement.street || ''}, ${abonement.postal_code || ''} ${abonement.city || ''}`.trim(),
// Shipping
shippingCustomerClass: isCompany ? '' : 'checked',
shippingCompanyClass: isCompany ? 'checked' : '',
shippingFullName: fullName,
shippingStreet: abonement.street || '',
shippingPostalCode: abonement.postal_code || '',
shippingCity: abonement.city || '',
shippingPhone: abonement.phone || '',
shippingEmail: abonement.email || '',
// Invoice same as shipping
invoiceSameAsShippingMark: invoiceSame ? '✓' : '',
// Invoice address
invoiceCompanyClass: isCompany ? 'checked' : '',
invoiceCustomerClass: isCompany ? '' : 'checked',
invoiceFullName: invoiceSame ? fullName : (abonement.invoice_full_name || ''),
invoiceStreet: invoiceSame ? (abonement.street || '') : (abonement.invoice_street || ''),
invoicePostalCode: invoiceSame ? (abonement.postal_code || '') : (abonement.invoice_postal_code || ''),
invoiceCity: invoiceSame ? (abonement.city || '') : (abonement.invoice_city || ''),
invoicePhone: invoiceSame ? (abonement.phone || '') : (abonement.invoice_phone || ''),
invoiceEmail: invoiceSame ? (abonement.email || '') : (abonement.invoice_email || ''),
// Company numbers (FN / ATU) — only shown for company users
fnCheckedClass: isCompany && fnNumber ? 'checked' : '',
fnNumber,
atuCheckedClass: isCompany && atuNumber ? 'checked' : '',
atuNumber,
// Unternehmer / Konsument
entrepreneurClass: isCompany ? 'checked' : '',
consumerClass: isCompany ? '' : 'checked',
// Product selection
selectedProductsHtml: this._buildSelectedProductsHtml(abonement),
// Payment
paymentSepaClass: abonement.payment_method === 'sepa' ? 'checked' : '',
paymentCardClass: abonement.payment_method === 'card' ? 'checked' : '',
paymentSofortClass: abonement.payment_method === 'sofort' ? 'checked' : '',
invoiceByEmailClass: abonement.invoice_by_email ? 'checked' : '',
// Signatures
profitplanetSignature,
companyStampImage: profitplanetSignature,
signatureImage: this._buildSignatureImgHtml(signatureDataUrl),
signingCity: signingCity || '',
fullName,
};
// DEBUG: log template source and variable keys with values
logger.info('AboContractService:render_debug', {
templateSource: template ? (template.includes('{{shippingFullName}}') ? 'has_placeholders' : 'NO_placeholders') : 'empty',
templateLen: template?.length,
variableKeys: Object.keys(variables),
sampleValues: {
shippingFullName: variables.shippingFullName,
shippingStreet: variables.shippingStreet,
shippingEmail: variables.shippingEmail,
recipientName: variables.recipientName,
},
});
const html = this._renderTemplate(template, variables, {
rawKeys: new Set(['selectedProductsHtml', 'profitplanetSignature', 'signatureImage', 'companyStampImage']),
});
const pdfBuffer = await this._renderPdfFromHtml(html);
const key = `abo/${abonement.id}/${contractKeyPart}.pdf`;
await sharedExoscaleClient.send(new PutObjectCommand({
Bucket: process.env.EXOSCALE_BUCKET,
Key: key,
Body: pdfBuffer,
ContentType: 'application/pdf',
}));
const recipientEmail = abonement.email || actorUser?.email;
if (recipientEmail) {
const isDe = lang === 'de';
const subject = isDe
? `ProfitPlanet Vertrag ${displayContractNumber}`
: `ProfitPlanet Contract ${displayContractNumber}`;
const text = isDe
? 'Ihr unterschriebener Vertrag ist als PDF im Anhang.'
: 'Your signed contract is attached as a PDF.';
const mailHtml = `<!doctype html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family:Arial,sans-serif;">
<p>${isDe ? 'Hallo' : 'Hi'},</p>
<p>${isDe ? 'Ihr unterschriebener Vertrag ist als PDF im Anhang.' : 'Your signed contract is attached as a PDF.'}</p>
<p>${isDe ? 'Vertragsnummer' : 'Contract number'}: <strong>${this._escapeHtml(displayContractNumber)}</strong></p>
<p>${isDe ? 'Viele Grüße' : 'Best regards'},<br>ProfitPlanet</p>
</body>
</html>`;
const mailPayload = {
subject,
text,
html: mailHtml,
lang,
attachments: [{
name: `${contractKeyPart}.pdf`,
content: pdfBuffer.toString('base64'),
}],
};
await MailService.sendAboContractEmail({ email: recipientEmail, ...mailPayload });
// When subscription is for someone else, also send the contract to the purchaser
const purchaserEmail = actorUser?.email;
const isForSomeoneElse = isForSelf === false;
if (isForSomeoneElse && purchaserEmail && purchaserEmail !== recipientEmail) {
try {
await MailService.sendAboContractEmail({ email: purchaserEmail, ...mailPayload });
} catch (e) {
logger.warn('AboContractService:purchaser_email_failed', { purchaserEmail, message: e?.message });
}
}
} else {
logger.warn('AboContractService:missing_recipient_email', { abonementId: abonement.id });
}
return { storageKey: key, contractNumber: displayContractNumber };
}
/**
* Generate sequential contract number: ABO-YYYY-NNNNN
* Uses MAX(id) from coffee_abonements as a simple sequence.
*/
async _generateSequentialContractNumber() {
const year = new Date().getFullYear();
const [rows] = await pool.query(
`SELECT COALESCE(MAX(id), 0) + 1 AS next_seq FROM coffee_abonements`
);
const seq = rows[0]?.next_seq || 1;
return `ABO-${year}-${String(seq).padStart(5, '0')}`;
}
/**
* Resolve user type from actorUser JWT payload or DB lookup.
*/
async _resolveUserType(actorUser) {
const fromToken = actorUser?.userType || actorUser?.user_type;
if (fromToken) return fromToken;
if (!actorUser?.id) return 'personal';
try {
const [rows] = await pool.query(
`SELECT user_type FROM users WHERE id = ? LIMIT 1`,
[actorUser.id]
);
return rows[0]?.user_type || 'personal';
} catch {
return 'personal';
}
}
/**
* Load company-specific data (FN number, ATU) for a company user.
*/
async _loadCompanyData(userId) {
try {
const [rows] = await pool.query(
`SELECT registration_number, atu_number FROM company_profiles WHERE user_id = ? LIMIT 1`,
[userId]
);
return {
registrationNumber: rows[0]?.registration_number || '',
atuNumber: rows[0]?.atu_number || '',
};
} catch {
return { registrationNumber: '', atuNumber: '' };
}
}
}
module.exports = AboContractService;

View File

@ -1,6 +1,7 @@
const pool = require('../../database/database'); const pool = require('../../database/database');
const AbonemmentRepository = require('../../repositories/abonemments/AbonemmentRepository'); const AbonemmentRepository = require('../../repositories/abonemments/AbonemmentRepository');
const InvoiceService = require('../invoice/InvoiceService'); // NEW const InvoiceService = require('../invoice/InvoiceService'); // NEW
const AboContractService = require('./AboContractService');
const UnitOfWork = require('../../database/UnitOfWork'); const UnitOfWork = require('../../database/UnitOfWork');
const ReferralService = require('../referral/ReferralService'); const ReferralService = require('../referral/ReferralService');
const ReferralTokenRepository = require('../../repositories/referral/ReferralTokenRepository'); const ReferralTokenRepository = require('../../repositories/referral/ReferralTokenRepository');
@ -10,6 +11,7 @@ class AbonemmentService {
constructor() { constructor() {
this.repo = new AbonemmentRepository(); this.repo = new AbonemmentRepository();
this.invoiceService = new InvoiceService(); // NEW this.invoiceService = new InvoiceService(); // NEW
this.aboContractService = new AboContractService();
} }
isAdmin(user) { isAdmin(user) {
@ -47,7 +49,6 @@ class AbonemmentService {
isForSelf, isForSelf,
recipientName, recipientName,
recipientEmail, recipientEmail,
recipientNotes,
firstName, firstName,
lastName, lastName,
email, email,
@ -56,7 +57,21 @@ class AbonemmentService {
city, city,
country, country,
frequency, frequency,
startDate, phone,
recipientContractName,
recipientAddress,
paymentMethod,
invoiceByEmail,
invoiceSameAsShipping,
invoiceFullName,
invoiceStreet,
invoicePostalCode,
invoiceCity,
invoicePhone,
invoiceEmail,
contractNumber,
signingCity,
signatureDataUrl,
actorUser, actorUser,
referredBy, // NEW: referred_by field referredBy, // NEW: referred_by field
}) { }) {
@ -71,7 +86,6 @@ class AbonemmentService {
city, city,
country, country,
frequency, frequency,
startDate,
}); });
const normalizedEmail = this.normalizeEmail(email); const normalizedEmail = this.normalizeEmail(email);
@ -113,15 +127,14 @@ class AbonemmentService {
} }
const now = new Date(); const now = new Date();
const startDateObj = startDate ? new Date(startDate) : now; const nextBilling = this.addInterval(now, billingInterval || 'month', intervalCount || 1);
const nextBilling = this.addInterval(startDateObj, billingInterval || 'month', intervalCount || 1);
const effectiveRecipientName = recipientName || `${firstName || ''} ${lastName || ''}`.trim() || null; const effectiveRecipientName = recipientName || `${firstName || ''} ${lastName || ''}`.trim() || null;
const effectiveEmail = forSelf ? normalizedEmail : normalizedRecipientEmail; const effectiveEmail = forSelf ? normalizedEmail : normalizedRecipientEmail;
const snapshot = { const snapshot = {
status: 'active', status: 'active',
started_at: startDateObj, started_at: now,
next_billing_at: nextBilling, next_billing_at: nextBilling,
billing_interval: billingInterval || 'month', billing_interval: billingInterval || 'month',
interval_count: intervalCount || 1, interval_count: intervalCount || 1,
@ -134,7 +147,6 @@ class AbonemmentService {
total_packs: totalPacks, total_packs: totalPacks,
is_for_self: forSelf, is_for_self: forSelf,
recipient_name: forSelf ? null : effectiveRecipientName, recipient_name: forSelf ? null : effectiveRecipientName,
recipient_notes: forSelf ? null : (recipientNotes || null)
}, },
pack_breakdown: breakdown, pack_breakdown: breakdown,
first_name: forSelf ? firstName : (effectiveRecipientName || firstName), first_name: forSelf ? firstName : (effectiveRecipientName || firstName),
@ -148,6 +160,19 @@ class AbonemmentService {
referred_by: referredBy || (forSelf ? (actorUser?.id ?? null) : null), referred_by: referredBy || (forSelf ? (actorUser?.id ?? null) : null),
user_id: forSelf ? (actorUser?.id ?? null) : null, user_id: forSelf ? (actorUser?.id ?? null) : null,
purchaser_user_id: actorUser?.id ?? null, // NEW: also store purchaser purchaser_user_id: actorUser?.id ?? null, // NEW: also store purchaser
// New fields
phone: phone || null,
recipient_name: recipientContractName || null,
recipient_address: recipientAddress || null,
payment_method: paymentMethod || null,
invoice_by_email: !!invoiceByEmail,
invoice_same_as_shipping: invoiceSameAsShipping !== false,
invoice_full_name: invoiceFullName || null,
invoice_street: invoiceStreet || null,
invoice_postal_code: invoicePostalCode || null,
invoice_city: invoiceCity || null,
invoice_phone: invoicePhone || null,
invoice_email: invoiceEmail || null,
}; };
console.log('[SUBSCRIBE ORDER] Snapshot user linking:', { console.log('[SUBSCRIBE ORDER] Snapshot user linking:', {
@ -166,6 +191,50 @@ class AbonemmentService {
pack_group: abonement?.pack_group, pack_group: abonement?.pack_group,
}); });
// NEW: issue ABO contract PDF (best-effort)
if (signatureDataUrl) {
try {
if (!abonement.contract_storage_key) {
const lang = actorUser?.lang || actorUser?.language || 'en';
const issued = await this.aboContractService.generateUploadAndEmail({
abonement,
actorUser,
contractNumber,
signingCity,
signatureDataUrl,
isForSelf: forSelf,
lang,
});
await this.repo.updateContractInfo(abonement.id, {
contractNumber: issued.contractNumber,
storageKey: issued.storageKey,
});
await this.repo.appendHistory(
abonement.id,
'abo_contract_issued',
actorUser?.id || null,
{ contractNumber: issued.contractNumber, storageKey: issued.storageKey, pack_group: abonement.pack_group },
new Date(),
);
}
} catch (e) {
console.error('[SUBSCRIBE ORDER] ABO contract generation failed:', e);
try {
await this.repo.appendHistory(
abonement.id,
'abo_contract_failed',
actorUser?.id || null,
{ message: e?.message || String(e), pack_group: abonement.pack_group },
new Date(),
);
} catch (_) {}
}
} else {
console.warn('[SUBSCRIBE ORDER] No signatureDataUrl provided; skipping ABO contract PDF generation.');
}
// NEW: issue invoice for first period and append history // NEW: issue invoice for first period and append history
try { try {
const invoice = await this.invoiceService.issueForAbonement( const invoice = await this.invoiceService.issueForAbonement(
@ -267,6 +336,9 @@ class AbonemmentService {
recipientName, recipientName,
recipientEmail, recipientEmail,
recipientNotes, recipientNotes,
contractNumber,
signingCity,
signatureDataUrl,
actorUser, actorUser,
referredBy, // NEW: referred_by field referredBy, // NEW: referred_by field
}) { }) {
@ -357,6 +429,52 @@ class AbonemmentService {
pack_group: abonement?.pack_group, pack_group: abonement?.pack_group,
}); });
// NEW: only generate contract when this is a new subscription (not merging into existing)
if (!existing) {
if (signatureDataUrl) {
try {
if (!abonement.contract_storage_key) {
const lang = actorUser?.lang || actorUser?.language || 'en';
const issued = await this.aboContractService.generateUploadAndEmail({
abonement,
actorUser,
contractNumber,
signingCity,
signatureDataUrl,
isForSelf: isForMe,
lang,
});
await this.repo.updateContractInfo(abonement.id, {
contractNumber: issued.contractNumber,
storageKey: issued.storageKey,
});
await this.repo.appendHistory(
abonement.id,
'abo_contract_issued',
actorUser?.id || null,
{ contractNumber: issued.contractNumber, storageKey: issued.storageKey, pack_group: abonement.pack_group },
new Date(),
);
}
} catch (e) {
console.error('[SUBSCRIBE] ABO contract generation failed:', e);
try {
await this.repo.appendHistory(
abonement.id,
'abo_contract_failed',
actorUser?.id || null,
{ message: e?.message || String(e), pack_group: abonement.pack_group },
new Date(),
);
} catch (_) {}
}
} else {
console.warn('[SUBSCRIBE] No signatureDataUrl provided; skipping ABO contract PDF generation.');
}
}
// NEW: issue invoice for first period and append history // NEW: issue invoice for first period and append history
try { try {
const invoice = await this.invoiceService.issueForAbonement( const invoice = await this.invoiceService.issueForAbonement(

View File

@ -0,0 +1,244 @@
const cron = require('node-cron');
const AbonemmentRepository = require('../../repositories/abonemments/AbonemmentRepository');
const InvoiceRepository = require('../../repositories/invoice/InvoiceRepository');
const InvoiceService = require('../invoice/InvoiceService');
const MailService = require('../email/MailService');
const { logger } = require('../../middleware/logger');
class RenewalCronService {
constructor() {
this.repo = new AbonemmentRepository();
this.invoiceRepo = new InvoiceRepository();
this.invoiceService = new InvoiceService();
}
addInterval(date, interval, count) {
const d = new Date(date);
if (interval === 'day') d.setDate(d.getDate() + count);
if (interval === 'week') d.setDate(d.getDate() + 7 * count);
if (interval === 'month') d.setMonth(d.getMonth() + count);
if (interval === 'year') d.setFullYear(d.getFullYear() + count);
return d;
}
/**
* Get the latest invoice for a subscription.
*/
async getLatestInvoice(abonementId) {
const invoices = await this.invoiceRepo.findByAbonement(abonementId);
return invoices.length ? invoices[0] : null; // already sorted DESC
}
// ──────────────────────────────────────────────
// Process active subscriptions due for renewal
//
// Simple prepaid flow:
// - Previous invoice paid (or first cycle) → generate new invoice + advance next_billing_at
// - Previous invoice NOT paid → skip (advance next_billing_at so we don't get stuck)
// - Gift-abo without registered user → skip (don't advance)
// ──────────────────────────────────────────────
async processActiveRenewals(now) {
let dueAbonements;
try {
dueAbonements = await this.repo.listDueForBilling(now);
} catch (err) {
logger.error('RenewalCron:fetch_due_error', { message: err?.message });
return;
}
if (!dueAbonements.length) {
logger.info('RenewalCron:no_due_subscriptions');
return;
}
logger.info('RenewalCron:found_due', { count: dueAbonements.length });
const results = { renewed: 0, skipped_unpaid: 0, skipped_gift: 0, skipped_other: 0, errors: [] };
for (const abon of dueAbonements) {
try {
// Re-check status (race-condition protection)
const fresh = await this.repo.getAbonementById(abon.id);
if (!fresh || fresh.status !== 'active') {
results.skipped_other++;
continue;
}
// Gift-abo where recipient hasn't registered yet → skip, don't advance
if (!fresh.user_id) {
logger.info('RenewalCron:gift_no_user', { abonementId: fresh.id });
results.skipped_gift++;
continue;
}
// Check last invoice payment status
const lastInvoice = await this.getLatestInvoice(fresh.id);
if (!lastInvoice || lastInvoice.status === 'paid') {
// First cycle or previous invoice paid → renew (generate invoice + advance)
await this.renewSingleAbonement(fresh);
results.renewed++;
} else {
// Previous invoice NOT paid → skip this cycle, just advance next_billing_at
const oldNextBilling = new Date(fresh.next_billing_at);
const newNextBilling = this.addInterval(
oldNextBilling,
fresh.billing_interval || 'month',
fresh.interval_count || 1,
);
await this.repo.transitionBilling(fresh.id, newNextBilling, {
event_type: 'skipped',
actor_user_id: null,
details: {
pack_group: fresh.pack_group,
trigger: 'auto_renewal_cron',
reason: 'previous_invoice_unpaid',
unpaid_invoice_id: lastInvoice.id,
previous_next_billing_at: oldNextBilling.toISOString(),
},
});
logger.info('RenewalCron:skipped_unpaid', {
abonementId: fresh.id,
unpaidInvoiceId: lastInvoice.id,
invoiceStatus: lastInvoice.status,
advancedTo: newNextBilling.toISOString(),
});
results.skipped_unpaid++;
}
} catch (err) {
results.errors.push({ abonementId: abon.id, message: err?.message });
logger.error('RenewalCron:renewal_failed', {
abonementId: abon.id,
message: err?.message,
stack: err?.stack,
});
}
}
logger.info('RenewalCron:phase_complete', results);
}
// ──────────────────────────────────────────────
// Helpers
// ──────────────────────────────────────────────
async renewSingleAbonement(abon) {
const abonId = abon.id;
const oldNextBilling = new Date(abon.next_billing_at);
const newNextBilling = this.addInterval(
oldNextBilling,
abon.billing_interval || 'month',
abon.interval_count || 1,
);
logger.info('RenewalCron:renewing', {
abonementId: abonId,
oldNextBilling: oldNextBilling.toISOString(),
newNextBilling: newNextBilling.toISOString(),
});
// Update billing date + history
const renewed = await this.repo.transitionBilling(abonId, newNextBilling, {
event_type: 'renewed',
actor_user_id: null,
details: {
pack_group: abon.pack_group,
trigger: 'auto_renewal_cron',
previous_next_billing_at: oldNextBilling.toISOString(),
},
});
// Issue new invoice
let invoice = null;
try {
const lang = abon.language || abon.lang || 'de';
invoice = await this.invoiceService.issueForAbonement(
renewed,
oldNextBilling,
newNextBilling,
{ actorUserId: null, lang },
);
logger.info('RenewalCron:invoice_issued', {
abonementId: abonId,
invoiceId: invoice?.id,
invoiceNumber: invoice?.invoice_number,
});
await this.repo.appendHistory(abonId, 'invoice_issued', null, {
pack_group: renewed.pack_group,
invoiceId: invoice.id,
trigger: 'auto_renewal_cron',
}, new Date());
} catch (invoiceErr) {
logger.error('RenewalCron:invoice_error', {
abonementId: abonId,
message: invoiceErr?.message,
});
}
// Send renewal email with invoice
try {
const recipientEmail = abon.email || invoice?.buyer_email;
if (recipientEmail) {
const lang = abon.language || abon.lang || 'de';
const customerName = [abon.first_name, abon.last_name].filter(Boolean).join(' ') || recipientEmail;
await MailService.sendRenewalEmail({
email: recipientEmail,
customerName,
invoiceNumber: invoice?.invoice_number || '-',
totalGross: invoice?.total_gross
? `${Number(invoice.total_gross).toFixed(2)} ${invoice.currency || abon.currency || 'EUR'}`
: '-',
nextBillingDate: newNextBilling.toISOString().slice(0, 10),
lang,
});
}
} catch (mailErr) {
logger.error('RenewalCron:renewal_email_error', {
abonementId: abonId,
message: mailErr?.message,
});
}
return { abonementId: abonId, invoiceId: invoice?.id || null };
}
// ──────────────────────────────────────────────
// Main entry point
// ──────────────────────────────────────────────
async processDueRenewals() {
const now = new Date();
logger.info('RenewalCron:tick_start', { now: now.toISOString() });
await this.processActiveRenewals(now);
logger.info('RenewalCron:tick_end', { now: now.toISOString() });
}
/**
* Start the cron schedule.
* Runs every day at 02:00 AM.
*/
start() {
cron.schedule('0 2 * * *', async () => {
try {
await this.processDueRenewals();
} catch (err) {
logger.error('RenewalCron:unhandled_error', {
message: err?.message,
stack: err?.stack,
});
}
});
logger.info('RenewalCron:scheduled', { schedule: '0 2 * * * (daily at 02:00)' });
}
}
module.exports = RenewalCronService;

View File

@ -232,6 +232,12 @@ class ContractUploadService {
let pdfBuffer, originalFilename, mimeType, fileSize; let pdfBuffer, originalFilename, mimeType, fileSize;
let contractBody; let contractBody;
const allowedContractTypes = new Set(['contract', 'gdpr', 'abo']);
const normalizedContractType = (contract_type === undefined || contract_type === null)
? 'contract'
: String(contract_type).trim().toLowerCase();
contract_type = allowedContractTypes.has(normalizedContractType) ? normalizedContractType : 'contract';
try { try {
// If templateId and lang are provided, fetch HTML template from object storage // If templateId and lang are provided, fetch HTML template from object storage
if (templateId && lang) { if (templateId && lang) {

View File

@ -78,6 +78,29 @@ class EmailVerificationService {
await UserStatusService.checkAndSetPendingIfComplete(userId, unitOfWork); await UserStatusService.checkAndSetPendingIfComplete(userId, unitOfWork);
logger.info('EmailVerificationService.verifyCode:pending_check_complete', { userId }); logger.info('EmailVerificationService.verifyCode:pending_check_complete', { userId });
// Auto-grant can_subscribe permission if user doesn't already have it
try {
const [permRows] = await unitOfWork.connection.query(
`SELECT id FROM permissions WHERE name = 'can_subscribe' AND is_active = TRUE LIMIT 1`
);
if (permRows.length > 0) {
const permId = permRows[0].id;
const [existing] = await unitOfWork.connection.query(
`SELECT 1 FROM user_permissions WHERE user_id = ? AND permission_id = ? LIMIT 1`,
[userId, permId]
);
if (existing.length === 0) {
await unitOfWork.connection.query(
`INSERT INTO user_permissions (user_id, permission_id) VALUES (?, ?)`,
[userId, permId]
);
logger.info('EmailVerificationService.verifyCode:can_subscribe_granted', { userId });
}
}
} catch (permError) {
logger.error('EmailVerificationService.verifyCode:permission_grant_error', { userId, error: permError.message });
}
logger.info('EmailVerificationService.verifyCode:success', { userId }); logger.info('EmailVerificationService.verifyCode:success', { userId });
return { success: true }; return { success: true };
} catch (error) { } catch (error) {

View File

@ -265,6 +265,40 @@ class MailService {
} }
} }
async sendAboContractEmail({ email, subject, text, html, lang, attachments = [] }) {
logger.info('MailService.sendAboContractEmail:start', { email, lang, hasHtml: Boolean(html), attachments: attachments.length });
try {
const payload = {
sender: this.sender,
to: [{ email }],
subject,
textContent: text
};
if (html) {
payload.htmlContent = html;
}
if (Array.isArray(attachments) && attachments.length) {
payload.attachment = attachments;
}
const data = await this.brevo.transactionalEmails.sendTransacEmail(payload);
logger.info('MailService.sendAboContractEmail:email_sent', { email, lang, hasHtml: Boolean(html), attachments: attachments.length });
return data;
} catch (error) {
const brevoError = this._extractBrevoErrorDetails(error);
logger.error('MailService.sendAboContractEmail:error', {
email,
lang,
message: error?.message,
brevoStatus: brevoError.status,
brevoData: brevoError.data
});
throw error;
}
}
async sendSubscriptionInvitationEmail({ email, inviterName, referralLink, lang = 'en' }) { async sendSubscriptionInvitationEmail({ email, inviterName, referralLink, lang = 'en' }) {
logger.info('MailService.sendSubscriptionInvitationEmail:start', { email, lang }); logger.info('MailService.sendSubscriptionInvitationEmail:start', { email, lang });
const isDe = lang === 'de'; const isDe = lang === 'de';
@ -342,6 +376,140 @@ class MailService {
} }
} }
async sendRenewalEmail({ email, customerName, invoiceNumber, totalGross, nextBillingDate, lang = 'en' }) {
logger.info('MailService.sendRenewalEmail:start', { email, lang, invoiceNumber });
const isDe = lang === 'de';
const subject = isDe
? `ProfitPlanet: Ihr Abonnement wurde verlängert Rechnung ${invoiceNumber}`
: `ProfitPlanet: Your subscription has been renewed Invoice ${invoiceNumber}`;
const safeName = this._escapeForHtml(customerName || '');
const safeInvoiceNumber = this._escapeForHtml(invoiceNumber || '');
const safeTotalGross = this._escapeForHtml(totalGross || '-');
const safeNextDate = this._escapeForHtml(nextBillingDate || '-');
const text = isDe
? [
`Hallo ${customerName || ''},`,
'',
'Ihr ProfitPlanet Kaffee-Abonnement wurde automatisch verlängert.',
'',
`Rechnungsnummer: ${invoiceNumber}`,
`Gesamtbetrag: ${totalGross}`,
`Nächste Verlängerung: ${nextBillingDate}`,
'',
'Ihre Rechnung ist als PDF im Anhang der vorherigen E-Mail enthalten.',
'Sie können Ihre Rechnungen auch jederzeit in Ihrem Dashboard einsehen.',
'',
'Falls Sie Ihr Abonnement pausieren oder kündigen möchten, können Sie dies in Ihrem Profil tun.',
'',
'Viele Grüße',
'Ihr ProfitPlanet Team',
].join('\n')
: [
`Hi ${customerName || ''},`,
'',
'Your ProfitPlanet coffee subscription has been automatically renewed.',
'',
`Invoice number: ${invoiceNumber}`,
`Total amount: ${totalGross}`,
`Next renewal: ${nextBillingDate}`,
'',
'Your invoice is attached as a PDF in the previous email.',
'You can also view all your invoices in your dashboard at any time.',
'',
'If you would like to pause or cancel your subscription, you can do so in your profile.',
'',
'Best regards,',
'Your ProfitPlanet Team',
].join('\n');
const logoUrl = process.env.MAIL_LOGO_URL || process.env.BREVO_LOGO_URL || process.env.APP_LOGO_URL || '';
const html = `<!doctype html>
<html>
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
<body style="margin:0;padding:0;background:#f5f7fb;font-family:Arial,sans-serif;color:#1f2937;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#f5f7fb;padding:24px 0;">
<tr>
<td align="center">
<table role="presentation" width="640" cellspacing="0" cellpadding="0" style="max-width:640px;background:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 8px 24px rgba(0,0,0,0.08);">
<tr>
<td style="padding:24px 28px;background:#111827;color:#ffffff;">
${logoUrl ? `<img src="${this._escapeForHtml(logoUrl)}" alt="ProfitPlanet" style="max-height:44px;display:block;margin-bottom:12px;">` : ''}
<h1 style="margin:0;font-size:22px;line-height:1.3;">${isDe ? 'Ihr Abonnement wurde verlängert' : 'Your subscription has been renewed'}</h1>
</td>
</tr>
<tr>
<td style="padding:24px 28px;">
<p style="margin:0 0 12px 0;font-size:15px;line-height:1.6;">${isDe ? 'Hallo' : 'Hi'} ${safeName},</p>
<p style="margin:0 0 18px 0;font-size:15px;line-height:1.6;">${isDe
? 'Ihr Kaffee-Abonnement wurde automatisch verlängert. Nachfolgend finden Sie die Details:'
: 'Your coffee subscription has been automatically renewed. Here are the details:'}</p>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;margin-bottom:18px;">
<tr>
<td style="padding:12px 14px;background:#f9fafb;font-size:13px;color:#6b7280;">${isDe ? 'Rechnungsnummer' : 'Invoice number'}</td>
<td style="padding:12px 14px;background:#f9fafb;font-size:13px;text-align:right;font-weight:700;">${safeInvoiceNumber}</td>
</tr>
<tr>
<td style="padding:12px 14px;font-size:13px;color:#6b7280;">${isDe ? 'Gesamtbetrag' : 'Total amount'}</td>
<td style="padding:12px 14px;font-size:13px;text-align:right;font-weight:700;">${safeTotalGross}</td>
</tr>
<tr>
<td style="padding:12px 14px;background:#f9fafb;font-size:13px;color:#6b7280;">${isDe ? 'Nächste Verlängerung' : 'Next renewal'}</td>
<td style="padding:12px 14px;background:#f9fafb;font-size:13px;text-align:right;font-weight:700;">${safeNextDate}</td>
</tr>
</table>
<p style="margin:0 0 18px 0;font-size:15px;line-height:1.6;">${isDe
? 'Ihre Rechnung mit PDF wurde Ihnen separat zugesendet. Sie können Ihre Rechnungen auch in Ihrem Dashboard einsehen.'
: 'Your invoice with PDF has been sent to you separately. You can also view your invoices in your dashboard.'}</p>
<p style="margin:0 0 8px 0;font-size:13px;color:#6b7280;line-height:1.5;">${isDe
? 'Falls Sie Ihr Abonnement pausieren oder kündigen möchten, können Sie dies jederzeit in Ihrem Profil tun.'
: 'If you would like to pause or cancel your subscription, you can do so anytime in your profile.'}</p>
</td>
</tr>
<tr>
<td style="padding:16px 28px;background:#f9fafb;border-top:1px solid #e5e7eb;">
<p style="margin:0;font-size:12px;color:#6b7280;">${isDe ? 'Viele Grüße Ihr ProfitPlanet Team' : 'Best regards Your ProfitPlanet Team'}</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`;
try {
const payload = {
sender: this.sender,
to: [{ email }],
subject,
textContent: text,
htmlContent: html,
};
const data = await this.brevo.transactionalEmails.sendTransacEmail(payload);
logger.info('MailService.sendRenewalEmail:email_sent', { email, lang, invoiceNumber });
return data;
} catch (error) {
const brevoError = this._extractBrevoErrorDetails(error);
logger.error('MailService.sendRenewalEmail:error', {
email,
lang,
invoiceNumber,
message: error?.message,
brevoStatus: brevoError.status,
brevoData: brevoError.data,
});
throw error;
}
}
_escapeForHtml(value) { _escapeForHtml(value) {
return String(value ?? '') return String(value ?? '')
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')

View File

@ -8,12 +8,114 @@ const { GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
const { s3: sharedExoscaleClient } = require('../../utils/exoscaleUploader'); const { s3: sharedExoscaleClient } = require('../../utils/exoscaleUploader');
const { logger } = require('../../middleware/logger'); const { logger } = require('../../middleware/logger');
const puppeteer = require('puppeteer'); const puppeteer = require('puppeteer');
const fs = require('fs/promises');
const path = require('path');
const CompanySettingsRepository = require('../../repositories/settings/CompanySettingsRepository'); const CompanySettingsRepository = require('../../repositories/settings/CompanySettingsRepository');
class InvoiceService { class InvoiceService {
constructor() { constructor() {
this.repo = new InvoiceRepository(); this.repo = new InvoiceRepository();
this._qrDataUriCache = new Map();
}
_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) => {
const re = new RegExp(`{{\\s*${name}\\s*}}`);
return re.test(template);
});
}
async _loadLocalInvoiceTemplateHtml() {
try {
const filePath = path.resolve(__dirname, '../../templates/invoice/invoiceTemplate.html');
return await fs.readFile(filePath, 'utf8');
} catch (e) {
logger.warn('InvoiceService._loadLocalInvoiceTemplateHtml:error', { message: e?.message });
return null;
}
}
_resolvePieceCountForQr(abonement) {
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;
}
_getLocalQrImagePath(pieceCount) {
const safePieceCount = pieceCount === 120 ? 120 : 60;
const fileName = safePieceCount === 120 ? 'qr_120.png' : 'qr_60.png';
return path.resolve(__dirname, '../../templates/invoice/qr', fileName);
}
async _getCompanySettingsQrDataUri(pieceCount) {
const safePieceCount = pieceCount === 120 ? 120 : 60;
try {
const repo = new CompanySettingsRepository();
const row = await repo.get();
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 _getLocalQrDataUri(pieceCount) {
const safePieceCount = pieceCount === 120 ? 120 : 60;
if (this._qrDataUriCache.has(safePieceCount)) {
return this._qrDataUriCache.get(safePieceCount);
}
const filePath = this._getLocalQrImagePath(safePieceCount);
try {
const buffer = await fs.readFile(filePath);
const dataUri = `data:image/png;base64,${buffer.toString('base64')}`;
this._qrDataUriCache.set(safePieceCount, dataUri);
return dataUri;
} catch (e) {
logger.warn('InvoiceService._getLocalQrDataUri:missing_qr_file', {
pieceCount: safePieceCount,
filePath,
message: e?.message,
});
return null;
}
}
async _buildQrCodeImageTag({ abonement }) {
const pieceCount = this._resolvePieceCountForQr(abonement);
if (!pieceCount) return '';
const dataUri = await this._getCompanySettingsQrDataUri(pieceCount) || await this._getLocalQrDataUri(pieceCount);
if (!dataUri) return '';
return `<img alt="QR Code" src="${this._escapeHtml(dataUri)}" />`;
} }
_escapeHtml(value) { _escapeHtml(value) {
@ -175,13 +277,47 @@ class InvoiceService {
Key: selected.storageKey, Key: selected.storageKey,
}); });
const obj = await sharedExoscaleClient.send(command); const obj = await sharedExoscaleClient.send(command);
return await this._s3BodyToString(obj.Body) || null; const html = await this._s3BodyToString(obj.Body) || null;
if (!html) return null;
return html;
} catch (e) { } catch (e) {
logger.warn('InvoiceService._loadInvoiceHtmlTemplate:error', { message: e?.message }); logger.warn('InvoiceService._loadInvoiceHtmlTemplate:error', { message: e?.message });
return null; return null;
} }
} }
_getProfitPlanetBankBlockHtml({ bankAccountHolder, bankIban, bankBic }) {
return `<strong>${this._escapeHtml(bankAccountHolder)}</strong><br>${this._escapeHtml(bankIban)}<br>${this._escapeHtml(bankBic)}`;
}
_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.
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',
bankIban: variables.bankIban || '',
bankBic: variables.bankBic || '',
});
const next = { ...variables };
if (!supportsBankVars) {
// Replace the default instruction text entirely with bank info
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;
}
async _buildInvoiceTemplateVariables({ invoice, items, abonement, lang }) { async _buildInvoiceTemplateVariables({ invoice, items, abonement, lang }) {
const isDe = lang === 'de'; const isDe = lang === 'de';
const isGift = abonement?.details?.is_for_self === false; const isGift = abonement?.details?.is_for_self === false;
@ -189,15 +325,33 @@ class InvoiceService {
const dueAt = invoice.due_at ? new Date(invoice.due_at).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 vatRate = invoice.vat_rate != null ? Number(invoice.vat_rate) : 0;
// Load company info from DB // Hardcoded bank info (Profit Planet)
let companyInfo = { company_name: 'ProfitPlanet GmbH', company_street: '', company_postal_city: '', company_country: 'Germany' }; const bankAccountHolder = 'Profit Planet GmbH';
try { const bankIban = 'AT16 2081 5000 4639 9507';
const repo = new CompanySettingsRepository(); const bankBic = 'STSPAT2GXXX';
const row = await repo.get();
if (row) companyInfo = row; // Hardcoded footer/contact info (Profit Planet)
} catch (e) { const footerText = [
logger.warn('InvoiceService._buildInvoiceTemplateVariables:company_settings_error', { message: e?.message }); 'Profit Planet GmbH',
} 'Kärntner Straße 227',
'8053 Graz',
'',
'Kontakt',
'Telefon: 0676 344 0274',
'E-Mail: office@profit-planet.com',
'',
'Profit Planet GmbH',
bankIban,
bankBic,
].join('<br>');
// Hardcoded company address (Profit Planet)
const companyInfo = {
company_name: 'Profit Planet GmbH',
company_street: 'Kärntner Straße 227',
company_postal_city: '8053 Graz',
company_country: '',
};
// For gift subscriptions: "Bill To" = recipient, "Ordered by" = purchaser // For gift subscriptions: "Bill To" = recipient, "Ordered by" = purchaser
// For self subscriptions: "Bill To" = the subscriber // For self subscriptions: "Bill To" = the subscriber
@ -223,6 +377,8 @@ class InvoiceService {
customerEmail = abonement?.email || invoice.buyer_email || ''; customerEmail = abonement?.email || invoice.buyer_email || '';
} }
const qrCodeImage = await this._buildQrCodeImageTag({ abonement });
return { return {
lang: isDe ? 'de' : 'en', lang: isDe ? 'de' : 'en',
documentTitle: isDe ? 'Rechnung' : 'Invoice', documentTitle: isDe ? 'Rechnung' : 'Invoice',
@ -263,9 +419,11 @@ class InvoiceService {
paymentInfoText: isDe paymentInfoText: isDe
? 'Bitte überweisen Sie den Gesamtbetrag unter Angabe der Rechnungsnummer.' ? 'Bitte überweisen Sie den Gesamtbetrag unter Angabe der Rechnungsnummer.'
: 'Please transfer the total amount stating the invoice number as reference.', : 'Please transfer the total amount stating the invoice number as reference.',
footerText: isDe bankAccountHolder: this._escapeHtml(bankAccountHolder),
? 'Vielen Dank für Ihr Vertrauen.' bankIban: this._escapeHtml(bankIban),
: 'Thank you for your business.', bankBic: this._escapeHtml(bankBic),
qrCodeImage,
footerText,
// Legacy key used by S3-stored templates // Legacy key used by S3-stored templates
itemsHtml: this._buildItemsHtml(items, invoice.currency), itemsHtml: this._buildItemsHtml(items, invoice.currency),
}; };
@ -276,7 +434,8 @@ class InvoiceService {
const template = await this._loadInvoiceHtmlTemplate(); const template = await this._loadInvoiceHtmlTemplate();
if (template) { if (template) {
return this._renderTemplate(template, variables); const varsForTemplate = this._prepareVariablesForTemplate(template, variables);
return this._renderTemplate(template, varsForTemplate);
} }
// Absolute fallback if template file is missing // Absolute fallback if template file is missing
@ -309,10 +468,11 @@ class InvoiceService {
}); });
const obj = await sharedExoscaleClient.send(command); const obj = await sharedExoscaleClient.send(command);
const html = await this._s3BodyToString(obj.Body); const html = await this._s3BodyToString(obj.Body);
return html || null; if (!html) return await this._loadLocalInvoiceTemplateHtml();
return html;
} catch (error) { } catch (error) {
logger.warn('InvoiceService._loadInvoiceTemplateHtml:error', { message: error?.message }); logger.warn('InvoiceService._loadInvoiceTemplateHtml:error', { message: error?.message });
return null; return await this._loadLocalInvoiceTemplateHtml();
} }
} }
@ -372,7 +532,30 @@ class InvoiceService {
let html = null; let html = null;
if (templateHtml) { if (templateHtml) {
html = this._renderTemplate(templateHtml, variables); 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 }); const htmlForPdf = html || await this._buildFallbackInvoiceHtml({ invoice, items, abonement, lang });

View File

@ -58,23 +58,26 @@ class UserStatusService {
const status = await repo.getStatusByUserId(userId); const status = await repo.getStatusByUserId(userId);
if (!status) return null; if (!status) return null;
// Guest users are always fully onboarded — skip quickaction flow // Guest users: check if email verification is still needed
if (status.status === 'active') { if (status.status === 'active') {
// Check if this is a guest user
const conn = unitOfWork.connection; const conn = unitOfWork.connection;
const [userRows] = await conn.query('SELECT role FROM users WHERE id = ? LIMIT 1', [userId]); const [userRows] = await conn.query('SELECT role FROM users WHERE id = ? LIMIT 1', [userId]);
if (userRows?.[0]?.role === 'guest') { if (userRows?.[0]?.role === 'guest') {
const allCompleteSteps = [ // For guests, only email_verified matters — other steps are auto-completed
{ key: 'email_verified', label: 'Email Verified', completed: true }, const emailVerified = !!status.email_verified;
const guestSteps = [
{ key: 'email_verified', label: 'Email Verified', completed: emailVerified },
{ key: 'profile_completed', label: 'Profile Completed', completed: true }, { key: 'profile_completed', label: 'Profile Completed', completed: true },
{ key: 'documents_uploaded', label: 'Documents Uploaded', completed: true }, { key: 'documents_uploaded', label: 'Documents Uploaded', completed: true },
{ key: 'contract_signed', label: 'Contract Signed', completed: true }, { key: 'contract_signed', label: 'Contract Signed', completed: true },
]; ];
const completedCount = guestSteps.filter(s => s.completed).length;
return { return {
status: 'active', status: 'active',
steps: allCompleteSteps, isGuest: true,
completedSteps: allCompleteSteps.map(s => s.label), steps: guestSteps,
progressPercent: 100, completedSteps: guestSteps.filter(s => s.completed).map(s => s.label),
progressPercent: Math.round((completedCount / guestSteps.length) * 100),
}; };
} }
} }

View File

@ -0,0 +1,70 @@
const pool = require('../../database/database');
class CoffeeShippingFeeService {
static _normalizePieceCount(value) {
const n = Number(value);
if (!Number.isInteger(n)) return null;
if (![60, 120].includes(n)) return null;
return n;
}
static _normalizePrice(value) {
const n = Number(value);
if (!Number.isFinite(n)) return null;
if (n < 0) return null;
return n;
}
static _mapRow(row) {
if (!row) return null;
return {
pieceCount: Number(row.piece_count),
price: Number(row.price),
};
}
static async list() {
const [rows] = await pool.query(
'SELECT piece_count, price FROM coffee_shipping_fees ORDER BY piece_count ASC'
);
return (rows || []).map((r) => this._mapRow(r));
}
static async get(pieceCount) {
const pc = this._normalizePieceCount(pieceCount);
if (!pc) return null;
const [rows] = await pool.query(
'SELECT piece_count, price FROM coffee_shipping_fees WHERE piece_count = ? LIMIT 1',
[pc]
);
return this._mapRow(rows?.[0]);
}
static async setPrice(pieceCount, price) {
const pc = this._normalizePieceCount(pieceCount);
if (!pc) {
const err = new Error('pieceCount must be 60 or 120');
err.status = 400;
throw err;
}
const p = this._normalizePrice(price);
if (p === null) {
const err = new Error('price must be a number >= 0');
err.status = 400;
throw err;
}
await pool.query(
`INSERT INTO coffee_shipping_fees (piece_count, price)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE price = VALUES(price)`,
[pc, p]
);
return this.get(pc);
}
}
module.exports = CoffeeShippingFeeService;

View File

@ -25,9 +25,13 @@ class DocumentTemplateService {
await uow.start(); await uow.start();
const allowed = ['personal','company','both']; const allowed = ['personal','company','both'];
const user_type = allowed.includes(data.user_type || data.userType) ? (data.user_type || data.userType) : 'both'; const user_type = allowed.includes(data.user_type || data.userType) ? (data.user_type || data.userType) : 'both';
const allowedContractTypes = ['contract', 'gdpr']; const allowedContractTypes = ['contract', 'gdpr', 'abo'];
const contract_type = (data.type === 'contract' && allowedContractTypes.includes(data.contract_type || data.contractType)) const rawContractType = (data.contract_type || data.contractType);
? (data.contract_type || data.contractType) const normalizedContractType = rawContractType !== undefined && rawContractType !== null
? String(rawContractType).trim().toLowerCase()
: rawContractType;
const contract_type = (data.type === 'contract' && allowedContractTypes.includes(normalizedContractType))
? normalizedContractType
: (data.type === 'contract' ? 'contract' : null); : (data.type === 'contract' ? 'contract' : null);
const created = await DocumentTemplateRepository.create({ ...data, user_type, contract_type }, uow.connection); const created = await DocumentTemplateRepository.create({ ...data, user_type, contract_type }, uow.connection);
await uow.commit(); await uow.commit();
@ -88,10 +92,11 @@ class DocumentTemplateService {
if (data.userType && !allowed.includes(data.userType)) delete data.userType; if (data.userType && !allowed.includes(data.userType)) delete data.userType;
if (data.user_type && !allowed.includes(data.user_type)) delete data.user_type; if (data.user_type && !allowed.includes(data.user_type)) delete data.user_type;
const nextType = data.type !== undefined ? data.type : current.type; const nextType = data.type !== undefined ? data.type : current.type;
const allowedContractTypes = ['contract', 'gdpr']; const allowedContractTypes = ['contract', 'gdpr', 'abo'];
const normalizeContractType = (value) => (value === undefined || value === null) ? value : String(value).trim().toLowerCase();
const contract_type = nextType === 'contract' const contract_type = nextType === 'contract'
? (allowedContractTypes.includes(data.contract_type || data.contractType || current.contract_type) ? (allowedContractTypes.includes(normalizeContractType(data.contract_type || data.contractType || current.contract_type))
? (data.contract_type || data.contractType || current.contract_type) ? normalizeContractType(data.contract_type || data.contractType || current.contract_type)
: 'contract') : 'contract')
: null; : null;
const newVersion = (current.version || 1) + 1; const newVersion = (current.version || 1) + 1;

View File

@ -48,9 +48,10 @@ class GuestUserService {
// Initialize user status as active (skip full registration flow for guests) // Initialize user status as active (skip full registration flow for guests)
await UserStatusService.initializeUserStatus(userId, 'personal', unitOfWork, 'active'); await UserStatusService.initializeUserStatus(userId, 'personal', unitOfWork, 'active');
// Mark ALL status flags as completed for guests — they skip the entire quickaction flow // Mark non-email status flags as completed for guests — they skip ID upload, profile, and contract
// email_verified stays FALSE so guests must verify their email on first login
await conn.query( await conn.query(
`UPDATE user_status SET email_verified = TRUE, profile_completed = TRUE, documents_uploaded = TRUE, contract_signed = TRUE, registration_completed = TRUE, is_admin_verified = TRUE WHERE user_id = ?`, `UPDATE user_status SET email_verified = FALSE, profile_completed = TRUE, documents_uploaded = TRUE, contract_signed = TRUE, registration_completed = TRUE, is_admin_verified = TRUE WHERE user_id = ?`,
[userId] [userId]
); );

View File

@ -0,0 +1,687 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ABO Vertrag Profit Planet GmbH</title>
<style>
/* Print setup */
@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;
}
.doc {
width: 100%;
max-width: 760px;
margin: 0 auto;
padding: 0 16px;
}
.header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
padding-bottom: 10px;
border-bottom: 2px solid var(--ink);
margin-bottom: 12px;
}
.brand {
min-width: 260px;
}
.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;
color: var(--ink);
max-width: 340px;
}
.company .muted { 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;
}
h3 {
margin: 12px 0 6px;
font-size: 11.5pt;
}
.grid2 {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
.box {
border: 1px solid var(--line);
border-radius: 8px;
padding: 10px;
background: var(--soft2);
}
.box .boxTitle {
font-weight: 700;
margin-bottom: 6px;
font-size: 10.5pt;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.row {
display: grid;
grid-template-columns: 1fr;
gap: 2px;
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: block; width: 100%; min-width: 0; }
.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;
}
.metaGrid .metaLabel {
white-space: nowrap;
}
.metaGrid .metaValue {
text-align: right;
}
.metaGrid .fill {
min-width: 160px;
}
.hint {
color: var(--muted);
font-size: 9.5pt;
margin-top: 6px;
}
.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-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
border-radius: 2px;
position: relative;
top: 2px;
flex: 0 0 auto;
}
.checkbox.checked::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 6px;
height: 10px;
border-right: 2px solid var(--ink);
border-bottom: 2px solid var(--ink);
transform: translate(-50%, -60%) rotate(40deg);
}
table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
background: #fff;
margin: 8px 0 0;
}
thead th {
text-align: left;
font-size: 10pt;
padding: 8px 10px;
background: var(--soft);
border-bottom: 1px solid var(--line);
}
tbody td {
padding: 8px 10px;
border-bottom: 1px solid #e5e7eb;
vertical-align: top;
font-size: 10.5pt;
}
tbody tr:last-child td { border-bottom: 0; }
.right { text-align: right; }
.muted { color: var(--muted); }
.pageBreak {
page-break-before: always;
break-before: page;
margin-top: 18px;
}
.para { margin: 0 0 10px; }
.para.tight { margin-bottom: 6px; }
.legal h3 {
margin-top: 14px;
margin-bottom: 6px;
font-size: 11.2pt;
}
.legal p {
margin: 0 0 10px;
text-align: justify;
}
.sigGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin-top: 18px;
}
.sig {
border-top: 1px solid #111827;
padding-top: 8px;
font-size: 10pt;
color: var(--muted);
}
.sig strong { color: var(--ink); }
.footerNote {
margin-top: 16px;
font-size: 9.5pt;
color: var(--muted);
}
@media print {
body { background: #fff; }
.box { background: #fff; }
thead th { background: #f3f4f6; }
/* Keep important blocks from being split across pages */
.header,
.grid2,
.box,
table,
thead,
tbody,
tr,
.sigGrid,
.signature-grid,
.signature-cell,
.signature-section {
break-inside: avoid;
page-break-inside: avoid;
-webkit-column-break-inside: avoid;
}
/* Keep labels/rows together */
.row {
break-inside: avoid;
page-break-inside: avoid;
}
/* Keep headings with the first lines of the following content */
h1, h2, h3 {
break-after: avoid-page;
page-break-after: avoid;
}
/* Improve paragraph pagination */
p {
orphans: 3;
widows: 3;
}
/* Repeat table header on new pages (Chromium/Puppeteer) */
thead { display: table-header-group; }
/* Explicit page breaks */
.pageBreak {
break-before: page;
page-break-before: always;
margin-top: 0;
}
}
</style>
</head>
<body>
<div class="doc">
<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>Liebenauer Hauptstraße 82c</div>
<div>A-8041 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>Vertrag über automatische Wiederbestellungen (ABO)</h1>
<p class="para muted" style="margin: 4px 0 0;">Bitte alle Felder vollständig ausfüllen und Zutreffendes ankreuzen.</p>
</div>
<div class="metaGrid muted">
<div class="metaLabel">Vertragsnummer:</div>
<div class="metaValue"><span class="fill">{{contractNumber}}</span></div>
<div class="metaLabel">Datum:</div>
<div class="metaValue"><span class="fill">{{currentDate}}</span></div>
</div>
</div>
<h2>An die</h2>
<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>
<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> &nbsp;&nbsp; <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">E-Mail-Adresse</div><div class="value"><span class="fill wide">{{shippingEmail}}</span></div></div>
<div class="row">
</div>
</div>
</div>
<div class="hint">Rechnungsadresse: <strong>{{invoiceSameAsShippingMark}} wie Lieferadresse</strong></div>
</div>
<div class="pageBreak"></div>
<h2>Rechnungsadresse (falls abweichend)</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> &nbsp;&nbsp; <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">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>
<h2>Zutreffendes bitte ankreuzen</h2>
<div class="box">
<div class="para tight">
<span class="check"><span class="checkbox {{entrepreneurClass}}"></span></span>
Der Kunde/Käufer tätigt das gegenständliche Rechtsgeschäft als Unternehmer im Sinne des § 1 Abs 1 Z 1 KSchG, das heißt, das Geschäft gehört zum Betrieb seines Unternehmens.
</div>
<div class="para tight">
<span class="check"><span class="checkbox {{consumerClass}}"></span></span>
Der Kunde/Käufer tätigt das gegenständliche Rechtsgeschäft als Konsument im Sinne des § 1 Abs 1 Z 2 KSchG.
</div>
</div>
<h2>Angebote</h2>
<div class="box">
<p class="para">
Mindestbestellmenge für BIO Kaffee und BIO Tee und BIO Kakao beträgt pro Bestellung jeweils <strong>120 Kapseln</strong>.
Preis pro Kapsel <strong>€ 2,97</strong> inkl. 20% MwSt. Preise und Konditionen gemäß gültigem PROFIT PLANET GMBH Tarif.
</p>
<table>
<thead>
<tr>
<th>Tarif</th>
<th class="right">Preis pro Kapsel</th>
</tr>
</thead>
<tbody>
<tr>
<td>Customer without abo</td>
<td class="right"><strong>2.97€</strong></td>
</tr>
<tr>
<td>Customer with abo</td>
<td class="right"><strong>1.77€</strong></td>
</tr>
</tbody>
</table>
</div>
<h2>Produktauswahl</h2>
<div class="box">
<p class="para muted" style="margin-bottom: 6px;">Superfood Coffee 60 Kapseln (bitte gewünschte Sorten ankreuzen / ergänzen)</p>
{{selectedProductsHtml}}
<p class="para" style="margin-top: 10px;">
Bei Angabe einer automatischen Wiederbestellung, gemäß den Regelungen in nachstehendem Punkt 3, erhält der Kunde in regelmäßigen Abständen,
<strong>BEGINNEND AM (Unterzeichnung des Vertrages)</strong> vorstehend eingetragene BIO Kaffee-Teemenge für die Dauer des Vertrages oder bis zum Widerruf der automatischen Wiederbestellung.
Der BIO Kaffee-Tee wird automatisch fakturiert und innerhalb von drei bis fünf Werktagen an den Kunden geliefert.
</p>
</div>
<div class="pageBreak"></div>
<h2>Zahlungsart</h2>
<div class="box">
<div class="checkline">
<span class="check"><span class="checkbox {{paymentSepaClass}}"></span> Sepa</span>
<span class="check"><span class="checkbox {{paymentCardClass}}"></span> Kreditkarte</span>
<span class="check"><span class="checkbox {{paymentSofortClass}}"></span> Sofortbanking</span>
</div>
<div class="checkline" style="margin-top: 2px;">
<span class="check"><span class="checkbox {{invoiceByEmailClass}}"></span> Bitte senden Sie mir meine Rechnung per E-Mail zu!</span>
</div>
</div>
<div class="legal">
<h2>§ 1 Vertragsgegenstand/Geltung der Allgemeinen Geschäftsbedingungen</h2>
<p>(1) 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, 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.</p>
<h2>§ 2 Laufzeit des Vertrages/Zahlung per Lastschrift/Abrechnung</h2>
<p>(1) Der Vertrag hat eine Laufzeit von 36 Monaten.</p>
<p>(2) 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.</p>
<p>(3) 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.</p>
<p>(4) Die Zahlung per Kreditkarte, Lastschrift/Bankeinzug, Rechnung und Nachnahme ist Voraussetzung für den Vertrag (SEPA Lastschrift-Mandat).</p>
<h2>§ 3 Automatische Wiederbestellungen</h2>
<p>(1) 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.</p>
<h2>§ 4 Verpflichtung zur Verwendung von Produkten der Profit Planet GmbH Vertragsstrafe/Liefervereinbarung</h2>
<p>(1) Der Kunde verpflichtet sich, während der Laufzeit des Vertrags auf den von Profit Planet GmbH zur Verfügung gestellten Kaffeemaschinen ausschließlich Produkte der Profit Planet GmbH „VITAPRESSO“ zu verwenden und einzusetzen, maximal jedoch für einen Zeitraum von drei (3) und/oder fünf (5) Jahren ab Vertragsschluss.</p>
<p>(2) Der Kaffee wird wiederkehrend zugestellt laut Bestellung.</p>
<p>(3) 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.</p>
</div>
<div class="pageBreak"></div>
<div class="legal">
<h2>§ 5 Wartung und Reparatur</h2>
<p>(1) Wartung und Reparaturen der Kaffeemaschinen sind in der Gestellung wie folgt enthalten:</p>
<p>(a) 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.</p>
<p>(b) 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.</p>
<p>(c) 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.</p>
<p>(d) 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.</p>
<h2>§ 6 Außerordentliche Kündigung</h2>
<p>(1) 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.</p>
<p>(2) Profit Planet GmbH ist insbesondere berechtigt, den Vertrag außerordentlich fristlos zu kündigen, wenn der Kunde</p>
<p>(a) 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</p>
<p>(b) eine in diesem Vertrag vereinbarte Mindestabnahmemenge in einem Zeitraum von 6 Monaten um mehr als durchschnittlich 20% unterschritten wurde, oder</p>
<p>(c) fremde Produkte auf den gestellten Kaffeemaschinen zubereitet.</p>
<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>
<h2>§ 7 Eigentumsverhältnisse</h2>
<p>Die gelieferten Maschinen bleiben Eigentum von Profit Planet GmbH.</p>
</div>
<div class="pageBreak"></div>
<div class="legal">
<h2>§ 8 Interne Bestellsysteme und Bestellungen, Datenschutz</h2>
<p>Weiteres stimme ich dem Erhalt von exklusiven Angeboten und Informationen wie folgt zu:</p>
<p><span class="check"><span class="checkbox checked"></span></span> 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><span class="check"><span class="checkbox checked"></span></span> 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>Mit dieser Unterschrift wird (werden) das (die) auf Seite 1 genannte(n) Gerät(e) zu genannten Konditionen übernommen.</p>
<p>Es gelten die Allgemeinen Geschäftsbedingungen der Profit Planet GmbH als vereinbart. Der Kunde erklärt hiermit ausdrücklich, dass er die Allgemeinen Geschäftsbedingungen und die Datenschutzbestimmungen gelesen hat und diesen zustimmt. Der Vertrag kommt mittels Annahme durch Profit Planet GmbH zustande und ist unter der Voraussetzung einer positiven Bonitätsprüfung gültig.</p>
<div class="box" style="margin-top: 14px;">
<div class="row"><div class="label">Ort</div><div class="value"><span class="fill wide">{{signingCity}}</span></div></div>
<div class="row"><div class="label">Datum</div><div class="value"><span class="fill wide">{{currentDate}}</span></div></div>
</div>
<div class="signature-section" style="margin-top: 30px; border-top: 1px solid #ccc; padding-top: 10px;">
<div class="signature-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start;">
<div class="signature-cell" style="text-align: center;">
<span class="signature-label" style="font-weight: bold;">Firmenstempel</span><br>
{{profitplanetSignature}}
</div>
<div class="signature-cell" style="text-align: center;">
<span class="signature-label" style="font-weight: bold;">Stempel / Unterschrift Kunde</span><br>
{{signatureImage}}
</div>
</div>
</div>
<p class="footerNote">Informationen über Inhaltsstoffe, Nährwertangaben etc. finden Sie auf der Lieferanten-Homepage www.lanaturalifestyle.com oder unter der Telefonnummer: 0043 552 322 960.</p>
</div>
<div class="pageBreak"></div>
<div class="legal">
<h2>Allgemeine Geschäftsbedingungen Kaffee-Service</h2>
<h3>§ 1 Geltungsbereich, Form</h3>
<p>(1) 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.</p>
<p>(2) 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.</p>
<p>(3) 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.</p>
<p>(4) 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.</p>
<p>(5) 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.</p>
<p>(6) 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.</p>
<h3>§ 2 Vertragsschluss</h3>
<p>(1) 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.</p>
<p>(2) 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.</p>
<p>(3) Die Annahme kann entweder schriftlich (z.B. durch Auftragsbestätigung) oder durch Auslieferung der Ware an den Käufer erklärt werden.</p>
</div>
<div class="pageBreak"></div>
<div class="legal">
<h3>§ 3 Lieferfrist und Lieferverzug</h3>
<p>(1) 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.</p>
<p>(2) 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.</p>
<p>(3) Der Eintritt unseres Lieferverzugs bestimmt sich nach den gesetzlichen Vorschriften. In jedem Fall ist aber eine Mahnung durch den Kunden erforderlich.</p>
<p>(4) 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.</p>
<h3>§ 4 Lieferung, Gefahrübergang, Abnahme, Annahmeverzug</h3>
<p>(1) 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.</p>
<p>(2) 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.</p>
<p>(3) 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.</p>
</div>
<div class="pageBreak"></div>
<div class="legal">
<h3>§ 5 Preise und Zahlungsbedingungen</h3>
<p>(1) Sofern im Einzelfall nichts anderes vereinbart ist, gelten unsere jeweils zum Zeitpunkt des Vertragsschlusses aktuellen Preise, und zwar ab Lager, zzgl. gesetzlicher Umsatzsteuer.</p>
<p>(2) 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.</p>
<p>(3) 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.</p>
<p>(4) 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.</p>
<p>(5) 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.</p>
<p>(6) 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.</p>
<h3>§ 6 Eigentumsvorbehalt</h3>
<p>(1) 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.</p>
<p>(2) 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 soweit Zugriffe Dritter (zB Pfändungen) auf die uns gehörenden Waren erfolgen.</p>
<p>(3) 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 heraus zu verlangen. Das Herausgabeverlangen beinhaltet nicht zugleich die Erklärung des Rücktritts; wir sind vielmehr berechtigt, lediglich die Ware heraus zu verlangen 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.</p>
</div>
<div class="pageBreak"></div>
<div class="legal"></div>
<h3>§ 7 Sachmängel</h3>
<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>
<h3>§ 8 Sonstige Haftung</h3>
<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 a) für Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit, 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über hinausgehendes 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>
<h3>§ 9 Verjährung</h3>
<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>
<h3>§ 10 Rechtswahl und Gerichtsstand</h3>
<p>(1) Für sämtliche Rechtsverhältnisse zwischen uns und dem Käufer gilt ausschließlich das materielle Recht 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>
<h3>§ 11 Schlussbestimmungen</h3>
<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 class="muted"><strong>Stand der Allgemeinen Geschäftsbedingungen Profit Planet Kaffee-Service: 01.08.2025</strong></p>
</div>
<div class="pageBreak"></div>
<div class="legal">
<h2>Informationen für Verbraucher über das Rücktrittsrecht (Widerrufsrecht)</h2>
<p><strong>Widerrufsrecht:</strong> 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><strong>Ausübung:</strong> 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><strong>Folgen:</strong> 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. 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>
<h2>Muster-Widerrufsformular</h2>
<div class="box">
<p class="para">(Wenn Sie den Vertrag widerrufen wollen, können Sie dieses Formular ausfüllen und an uns zurücksenden.)</p>
<p class="para"> An Profit Planet GmbH, Liebenauer Hauptstraße 82c, 8041 Graz, E-Mail: office@profit-planet.com:</p>
<p class="para"> Hiermit widerrufe(n) ich/wir ( <span class="fill">&nbsp;</span> )</p>
<p class="para">den von mir/uns ( <span class="fill">&nbsp;</span> ) abgeschlossenen Vertrag über den Kauf der folgenden Ware(n)/die Erbringung der folgenden Dienstleistung</p>
<p class="para"> Bestellt am ( <span class="fill">&nbsp;</span> ) / erhalten am ( <span class="fill">&nbsp;</span> )</p>
<p class="para"> Name des/der Verbraucher(s): <span class="fill wide">&nbsp;</span></p>
<p class="para"> Anschrift des/der Verbraucher(s): <span class="fill full">&nbsp;</span></p>
<p class="para"> Datum: <span class="fill">&nbsp;</span></p>
<p class="para"> Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier): <span class="fill wide">&nbsp;</span></p>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,676 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ABO Vertrag Profit Planet GmbH</title>
<style>
/* Print setup */
@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;
}
.doc {
width: 100%;
margin: 0 auto;
}
.header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
padding-bottom: 10px;
border-bottom: 2px solid var(--ink);
margin-bottom: 12px;
}
.brand {
min-width: 260px;
}
.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;
color: var(--ink);
max-width: 340px;
}
.company .muted { 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;
}
h3 {
margin: 12px 0 6px;
font-size: 11.5pt;
}
.grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.box {
border: 1px solid var(--line);
border-radius: 8px;
padding: 10px;
background: var(--soft2);
}
.box .boxTitle {
font-weight: 700;
margin-bottom: 6px;
font-size: 10.5pt;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.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%; }
.hint {
color: var(--muted);
font-size: 9.5pt;
margin-top: 6px;
}
.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: 0px;
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);
border-radius: 8px;
overflow: hidden;
background: #fff;
margin: 8px 0 0;
}
thead th {
text-align: left;
font-size: 10pt;
padding: 8px 10px;
background: var(--soft);
border-bottom: 1px solid var(--line);
}
tbody td {
padding: 8px 10px;
border-bottom: 1px solid #e5e7eb;
vertical-align: top;
font-size: 10.5pt;
}
tbody tr:last-child td { border-bottom: 0; }
.right { text-align: right; }
.muted { color: var(--muted); }
.pageBreak {
page-break-before: always;
break-before: page;
margin-top: 18px;
}
.para { margin: 0 0 10px; }
.para.tight { margin-bottom: 6px; }
.legal h3 {
margin-top: 14px;
margin-bottom: 6px;
font-size: 11.2pt;
}
.legal p {
margin: 0 0 10px;
text-align: justify;
}
.sigGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin-top: 18px;
}
.sig {
border-top: 1px solid #111827;
padding-top: 8px;
font-size: 10pt;
color: var(--muted);
}
.sig strong { color: var(--ink); }
.footerNote {
margin-top: 16px;
font-size: 9.5pt;
color: var(--muted);
}
@media print {
body { background: #fff; }
.box { background: #fff; }
thead th { background: #f3f4f6; }
/* Keep important blocks from being split across pages */
.header,
.grid2,
.box,
table,
thead,
tbody,
tr,
.sigGrid,
.signature-grid,
.signature-cell,
.signature-section {
break-inside: avoid;
page-break-inside: avoid;
-webkit-column-break-inside: avoid;
}
/* Keep labels/rows together */
.row {
break-inside: avoid;
page-break-inside: avoid;
}
/* Force specific blocks (like §7) to stay together */
.keepTogether {
break-inside: avoid;
page-break-inside: avoid;
-webkit-column-break-inside: avoid;
}
/* Keep headings with the first lines of the following content */
h1, h2, h3 {
break-after: avoid-page;
page-break-after: avoid;
}
/* Improve paragraph pagination */
p {
orphans: 3;
widows: 3;
}
/* Repeat table header on new pages (Chromium/Puppeteer) */
thead { display: table-header-group; }
/* Explicit page breaks */
.pageBreak {
break-before: page;
page-break-before: always;
margin-top: 0;
}
}
</style>
</head>
<body>
<div class="doc">
<div class="header">
<div class="brand">
<p class="title">ABO Vertrag</p>
<p class="subtitle">Kaffee-/Tee-Service & automatische Wiederbestellungen</p>
</div>
<div class="company">
<div><strong>PROFIT PLANET GMBH</strong></div>
<div>Liebenauer Hauptstraße 82c</div>
<div>A-8041 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>Vertrag über automatische Wiederbestellungen (ABO)</h1>
<p class="para muted" style="margin: 4px 0 0;">Bitte alle Felder vollständig ausfüllen und Zutreffendes ankreuzen.</p>
</div>
<div style="text-align:right; font-size: 10pt;" class="muted">
<div>Vertragsnummer: <span class="fill">{{contractNumber}}</span></div>
<div>Datum: <span class="fill">{{currentDate}}</span></div>
</div>
</div>
<h2>An die</h2>
<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="grid2" style="margin-top: 10px;">
<div class="box">
<div class="boxTitle">Affiliate</div>
<div class="row">
<div class="label">AFFILIATE NAME</div>
<div class="value"><span class="fill wide">{{affiliateName}}</span></div>
</div>
</div>
<div class="box">
<div class="boxTitle">Client</div>
<div class="row">
<div class="label">CLIENT NAME</div>
<div class="value"><span class="fill wide">{{clientName}}</span></div>
</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> &nbsp;&nbsp; <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">{{shippingMobile}}</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 class="row">
</div>
</div>
</div>
<div class="hint">Rechnungsadresse: <strong>{{invoiceSameAsShippingMark}} wie Lieferadresse</strong></div>
</div>
<h2>Rechnungsadresse (falls abweichend)</h2>
<div class="box">
<div class="grid2">
<div>
<div class="checkline" style="padding-top: 0;">
<span class="check"><span class="checkbox {{invoiceCompanyClass}}"></span> FIRMA</span>
<span class="check"><span class="checkbox {{invoiceCustomerClass}}"></span> KUNDE</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> &nbsp;&nbsp; <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">{{invoiceMobile}}</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 class="row">
<div class="label">Bevorzugte Kontaktaufnahme</div>
<div class="value">
<span class="check"><span class="checkbox {{invoicePrefPhoneClass}}"></span> Telefon</span>
<span class="check" style="margin-left: 12px;"><span class="checkbox {{invoicePrefEmailClass}}"></span> E-Mail</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>
<span class="muted" style="margin-left:auto; font-size: 9.5pt;">(falls zutreffend ausfüllen)</span>
</div>
</div>
<h2>Zutreffendes bitte ankreuzen</h2>
<div class="box">
<div class="para tight">
<span class="check"><span class="checkbox {{entrepreneurClass}}"></span></span>
Der Kunde/Käufer tätigt das gegenständliche Rechtsgeschäft als Unternehmer im Sinne des § 1 Abs 1 Z 1 KSchG, das heißt, das Geschäft gehört zum Betrieb seines Unternehmens.
</div>
<div class="para tight">
<span class="check"><span class="checkbox {{consumerClass}}"></span></span>
Der Kunde/Käufer tätigt das gegenständliche Rechtsgeschäft als Konsument im Sinne des § 1 Abs 1 Z 2 KSchG.
</div>
</div>
<div class="pageBreak"></div>
<h2>Angebote</h2>
<div class="box">
<p class="para">
Mindestbestellmenge für BIO Kaffee und BIO Tee und BIO Kakao beträgt pro Bestellung jeweils <strong>120 Kapseln</strong>.
Preis pro Kapsel <strong>€ 2,97</strong> inkl. 20% MwSt. Preise und Konditionen gemäß gültigem PROFIT PLANET GMBH Tarif.
</p>
<table>
<thead>
<tr>
<th>Tarif</th>
<th class="right">Preis pro Kapsel</th>
</tr>
</thead>
<tbody>
<tr>
<td>Customer without abo</td>
<td class="right"><strong>2.97€</strong></td>
</tr>
<tr>
<td>Customer with abo</td>
<td class="right"><strong>1.47€</strong></td>
</tr>
</tbody>
</table>
</div>
<h2>Produktauswahl</h2>
<div class="box">
<p class="para muted" style="margin-bottom: 6px;">Superfood Coffee 60 Kapseln (bitte gewünschte Sorten ankreuzen / ergänzen)</p>
{{selectedProductsHtml}}
<p class="para" style="margin-top: 10px;">
Bei Angabe einer automatischen Wiederbestellung, gemäß den Regelungen in nachstehendem Punkt 3, erhält der Kunde in regelmäßigen Abständen,
<strong>BEGINNEND AM (Unterzeichnung des Vertrages)</strong> vorstehend eingetragene BIO Kaffee-Teemenge für die Dauer des Vertrages oder bis zum Widerruf der automatischen Wiederbestellung.
Der BIO Kaffee-Tee wird automatisch im Abstand von (zutreffendes bitte ankreuzen)
</p>
<div class="checkline" style="margin-top: 2px;">
<span class="check"><span class="checkbox {{intervalMonthlyClass}}"></span> 1 Monat</span>
<span class="check"><span class="checkbox {{intervalTwoMonthlyClass}}"></span> 2 Monate</span>
<span class="check"><span class="checkbox {{intervalQuarterlyClass}}"></span> 3 Monate</span>
</div>
<p class="para">fakturiert und innerhalb von drei bis fünf Werktagen an den Kunden geliefert.</p>
</div>
<h2>Zahlungsart</h2>
<div class="box">
<div class="checkline">
<span class="check"><span class="checkbox {{paymentSepaClass}}"></span> Sepa</span>
<span class="check"><span class="checkbox {{paymentCardClass}}"></span> Kreditkarte</span>
<span class="check"><span class="checkbox {{paymentSofortClass}}"></span> Sofortbanking</span>
</div>
<div class="checkline" style="margin-top: 2px;">
<span class="check"><span class="checkbox {{invoiceByEmailClass}}"></span> Bitte senden Sie mir meine Rechnung per E-Mail zu!</span>
</div>
</div>
<div class="pageBreak"></div>
<div class="legal">
<h2>§ 1 Vertragsgegenstand/Geltung der Allgemeinen Geschäftsbedingungen</h2>
<p>(1) 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, 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.</p>
<h2>§ 2 Laufzeit des Vertrages/Zahlung per Lastschrift/Abrechnung</h2>
<p>(1) Der Vertrag hat eine Laufzeit von 36 Monaten.</p>
<p>(2) 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.</p>
<p>(3) 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.</p>
<p>(4) Die Zahlung per Kreditkarte, Lastschrift/Bankeinzug, Rechnung und Nachnahme ist Voraussetzung für den Vertrag (SEPA Lastschrift-Mandat).</p>
<h2>§ 3 Automatische Wiederbestellungen</h2>
<p>(1) 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.</p>
<h2>§ 4 Verpflichtung zur Verwendung von Produkten der Profit Planet GmbH Vertragsstrafe/Liefervereinbarung</h2>
<p>(1) Der Kunde verpflichtet sich, während der Laufzeit des Vertrags auf den von Profit Planet GmbH zur Verfügung gestellten Kaffeemaschinen ausschließlich Produkte der Profit Planet GmbH „VITAPRESSO“ zu verwenden und einzusetzen, maximal jedoch für einen Zeitraum von drei (3) und/oder fünf (5) Jahren ab Vertragsschluss.</p>
<p>(2) Der Kaffee wird wiederkehrend zugestellt laut Bestellung.</p>
<p>(3) 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.</p>
<div class="pageBreak"></div>
<h2>§ 5 Wartung und Reparatur</h2>
<p>(1) Wartung und Reparaturen der Kaffeemaschinen sind in der Gestellung wie folgt enthalten:</p>
<p>(a) 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.</p>
<p>(b) 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.</p>
<p>(c) 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.</p>
<p>(d) 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.</p>
<h2>§ 6 Außerordentliche Kündigung</h2>
<p>(1) 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.</p>
<p>(2) Profit Planet GmbH ist insbesondere berechtigt, den Vertrag außerordentlich fristlos zu kündigen, wenn der Kunde</p>
<p>(a) 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</p>
<p>(b) eine in diesem Vertrag vereinbarte Mindestabnahmemenge in einem Zeitraum von 6 Monaten um mehr als durchschnittlich 20% unterschritten wurde, oder</p>
<p>(c) fremde Produkte auf den gestellten Kaffeemaschinen zubereitet.</p>
<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>
<div class="keepTogether">
<h2>§ 7 Eigentumsverhältnisse</h2>
<p>Die gelieferten Maschinen bleiben Eigentum von Profit Planet GmbH.</p>
</div>
<div class="pageBreak"></div>
<h2>§ 8 Interne Bestellsysteme und Bestellungen, Datenschutz</h2>
<p>Weiteres stimme ich dem Erhalt von exklusiven Angeboten und Informationen wie folgt zu:</p>
<p><span class="check"><span class="checkbox checked"></span></span> 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><span class="check"><span class="checkbox checked"></span></span> 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>Mit dieser Unterschrift wird (werden) das (die) auf Seite 1 genannte(n) Gerät(e) zu genannten Konditionen übernommen.</p>
<p>Es gelten die Allgemeinen Geschäftsbedingungen der Profit Planet GmbH als vereinbart. Der Kunde erklärt hiermit ausdrücklich, dass er die Allgemeinen Geschäftsbedingungen und die Datenschutzbestimmungen gelesen hat und diesen zustimmt. Der Vertrag kommt mittels Annahme durch Profit Planet GmbH zustande und ist unter der Voraussetzung einer positiven Bonitätsprüfung gültig.</p>
<div class="box" style="margin-top: 14px;">
<div class="row"><div class="label">Ort</div><div class="value"><span class="fill wide">{{shippingCity}}</span></div></div>
<div class="row"><div class="label">Datum</div><div class="value"><span class="fill wide">{{currentDate}}</span></div></div>
</div>
<div class="signature-section" style="margin-top: 30px; border-top: 1px solid #ccc; padding-top: 10px;">
<div class="signature-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start;">
<div class="signature-cell" style="text-align: center;">
<span class="signature-label" style="font-weight: bold;">Unterschrift Profit Planet</span><br>
{{profitplanetSignature}}
</div>
<div class="signature-cell" style="text-align: center;">
<span class="signature-label" style="font-weight: bold;">Stempel / Unterschrift Kunde</span><br>
{{signatureImage}}
</div>
</div>
<div class="signature-fullname" style="margin-top: 10px; font-size: 0.95em;">
fullname: {{fullName}}
</div>
<div class="signature-date" style="margin-top: 4px; font-size: 0.95em;">
date: {{currentDate}}
</div>
</div>
<p class="footerNote">Informationen über Inhaltsstoffe, Nährwertangaben etc. finden Sie auf der Lieferanten-Homepage www.lanaturalifestyle.com oder unter der Telefonnummer: 0043 552 322 960.</p>
<div class="pageBreak"></div>
<h2>Allgemeine Geschäftsbedingungen Kaffee-Service</h2>
<h3>§ 1 Geltungsbereich, Form</h3>
<p>(1) 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.</p>
<p>(2) 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.</p>
<p>(3) 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.</p>
<p>(4) 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.</p>
<p>(5) 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.</p>
<p>(6) 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.</p>
<h3>§ 2 Vertragsschluss</h3>
<p>(1) 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.</p>
<p>(2) 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.</p>
<p>(3) Die Annahme kann entweder schriftlich (z.B. durch Auftragsbestätigung) oder durch Auslieferung der Ware an den Käufer erklärt werden.</p>
<h3>§ 3 Lieferfrist und Lieferverzug</h3>
<p>(1) 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.</p>
<p>(2) 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.</p>
<p>(3) Der Eintritt unseres Lieferverzugs bestimmt sich nach den gesetzlichen Vorschriften. In jedem Fall ist aber eine Mahnung durch den Kunden erforderlich.</p>
<p>(4) 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.</p>
<h3>§ 4 Lieferung, Gefahrübergang, Abnahme, Annahmeverzug</h3>
<p>(1) 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.</p>
<p>(2) 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.</p>
<p>(3) 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.</p>
<h3>§ 5 Preise und Zahlungsbedingungen</h3>
<p>(1) Sofern im Einzelfall nichts anderes vereinbart ist, gelten unsere jeweils zum Zeitpunkt des Vertragsschlusses aktuellen Preise, und zwar ab Lager, zzgl. gesetzlicher Umsatzsteuer.</p>
<p>(2) 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.</p>
<p>(3) 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.</p>
<p>(4) 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.</p>
<p>(5) 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.</p>
<p>(6) 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.</p>
<h3>§ 6 Eigentumsvorbehalt</h3>
<p>(1) 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.</p>
<p>(2) 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 soweit Zugriffe Dritter (zB Pfändungen) auf die uns gehörenden Waren erfolgen.</p>
<p>(3) 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 heraus zu verlangen. Das Herausgabeverlangen beinhaltet nicht zugleich die Erklärung des Rücktritts; wir sind vielmehr berechtigt, lediglich die Ware heraus zu verlangen 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.</p>
<h3>§ 7 Sachmängel</h3>
<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>
<h3>§ 8 Sonstige Haftung</h3>
<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 a) für Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit, 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über hinausgehendes 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>
<h3>§ 9 Verjährung</h3>
<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>
<h3>§ 10 Rechtswahl und Gerichtsstand</h3>
<p>(1) Für sämtliche Rechtsverhältnisse zwischen uns und dem Käufer gilt ausschließlich das materielle Recht 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>
<h3>§ 11 Schlussbestimmungen</h3>
<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 class="muted"><strong>Stand der Allgemeinen Geschäftsbedingungen Profit Planet Kaffee-Service: 01.08.2025</strong></p>
<div class="pageBreak"></div>
<h2>Informationen für Verbraucher über das Rücktrittsrecht (Widerrufsrecht)</h2>
<p><strong>Widerrufsrecht:</strong> 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><strong>Ausübung:</strong> 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><strong>Folgen:</strong> 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. 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>
<h2>Muster-Widerrufsformular</h2>
<div class="box">
<p class="para">(Wenn Sie den Vertrag widerrufen wollen, können Sie dieses Formular ausfüllen und an uns zurücksenden.)</p>
<p class="para"> An Profit Planet GmbH, Liebenauer Hauptstraße 82c, 8041 Graz, E-Mail: office@profit-planet.com:</p>
<p class="para"> Hiermit widerrufe(n) ich/wir ( <span class="fill">&nbsp;</span> )</p>
<p class="para">den von mir/uns ( <span class="fill">&nbsp;</span> ) abgeschlossenen Vertrag über den Kauf der folgenden Ware(n)/die Erbringung der folgenden Dienstleistung</p>
<p class="para"> Bestellt am ( <span class="fill">&nbsp;</span> ) / erhalten am ( <span class="fill">&nbsp;</span> )</p>
<p class="para"> Name des/der Verbraucher(s): <span class="fill wide">&nbsp;</span></p>
<p class="para"> Anschrift des/der Verbraucher(s): <span class="fill full">&nbsp;</span></p>
<p class="para"> Datum: <span class="fill">&nbsp;</span></p>
<p class="para"> Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier): <span class="fill wide">&nbsp;</span></p>
</div>
</div>
</div>
</body>
</html>

View File

@ -179,6 +179,41 @@
line-height: 1.6; line-height: 1.6;
} }
.payment-grid {
display: flex;
justify-content: space-between;
gap: 20px;
align-items: flex-start;
}
.payment-text {
flex: 1;
min-width: 0;
}
.payment-qr {
flex: 0 0 auto;
width: 170px;
text-align: right;
}
.payment-qr img {
width: 160px;
height: 160px;
object-fit: contain;
display: inline-block;
background: #ffffff;
border-radius: 6px;
padding: 6px;
}
@media (max-width: 560px) {
.payment-grid {
flex-direction: column;
}
.payment-qr {
width: auto;
text-align: left;
}
}
/* ── Footer ────────────────────────────────── */ /* ── Footer ────────────────────────────────── */
.footer { .footer {
border-top: 1px solid #e5e7eb; border-top: 1px solid #e5e7eb;
@ -195,7 +230,7 @@
<!-- Header --> <!-- Header -->
<div class="header"> <div class="header">
<div class="header-left"> <div class="header-left">
<div class="company-name">{{companyName}}</div> <div class="company-name">Profit Planet GmbH</div>
<div class="company-sub">Coffee Subscription Service</div> <div class="company-sub">Coffee Subscription Service</div>
</div> </div>
<div class="header-right"> <div class="header-right">
@ -210,10 +245,9 @@
<div class="meta-block"> <div class="meta-block">
<h3>{{fromLabel}}</h3> <h3>{{fromLabel}}</h3>
<p> <p>
<span class="highlight">{{companyName}}</span><br> <span class="highlight">Profit Planet GmbH</span><br>
{{companyStreet}}<br> Kärntner Straße 227<br>
{{companyPostalCity}}<br> 8053 Graz
{{companyCountry}}
</p> </p>
</div> </div>
<div class="meta-block"> <div class="meta-block">
@ -273,13 +307,24 @@
<!-- Payment Info --> <!-- Payment Info -->
<div class="payment-info"> <div class="payment-info">
<h3>{{paymentInfoTitle}}</h3> <h3>PAYMENT INFORMATION</h3>
<p>{{paymentInfoText}}</p> <div class="payment-grid">
<div class="payment-text">
<p>
<strong>Profit Planet GmbH</strong><br>
AT16 2081 5000 4639 9507<br>
STSPAT2GXXX
</p>
<p>
Please use the Invoice number as a reference when making the payment.
</p>
</div>
<div class="payment-qr">{{qrCodeImage}}</div>
</div>
</div> </div>
<!-- Footer --> <!-- Footer -->
<div class="footer"> <div class="footer">
{{companyName}} &middot; {{companyStreet}} &middot; {{companyPostalCity}} &middot; {{companyCountry}}<br>
{{footerText}} {{footerText}}
</div> </div>
</div> </div>

View File

@ -0,0 +1,6 @@
Place the locally-stored QR code images here:
- `qr_60.png` — QR for the 60-piece abo
- `qr_120.png` — QR for the 120-piece abo
The invoice PDF renderer embeds these files as base64 data-URIs so Puppeteer can render them without network access.

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB