mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 03:26:40 +02:00
The Lexware Office API requires voucherStatus as a mandatory parameter for the /v1/voucherlist endpoint. Without it, the API returns 400 Bad Request. Using 'any' fetches vouchers regardless of status. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
382 lines
10 KiB
TypeScript
382 lines
10 KiB
TypeScript
// ============================================================
|
|
// Lexware Office - Voucher Operations Service
|
|
// ============================================================
|
|
|
|
import {
|
|
Injectable,
|
|
Logger,
|
|
NotFoundException,
|
|
ConflictException,
|
|
} from '@nestjs/common';
|
|
import { Prisma } from '.prisma/crm-client';
|
|
import { CrmPrismaService } from '../prisma/crm-prisma.service';
|
|
import { LexwareClientService } from './lexware-client.service';
|
|
import { QueryLexwareVouchersDto } from './dto/query-lexware-vouchers.dto';
|
|
import {
|
|
LexwareVoucherListResponse,
|
|
LexwareVoucherDetail,
|
|
} from './interfaces/lexware-api.interfaces';
|
|
import {
|
|
voucherTypeFromLexware,
|
|
voucherTypeToLexwareEndpoint,
|
|
voucherDetailToCacheData,
|
|
} from './utils/lexware-mapper';
|
|
|
|
@Injectable()
|
|
export class LexwareVouchersService {
|
|
private readonly logger = new Logger(LexwareVouchersService.name);
|
|
|
|
constructor(
|
|
private readonly prisma: CrmPrismaService,
|
|
private readonly lexwareClient: LexwareClientService,
|
|
) {}
|
|
|
|
// --------------------------------------------------------
|
|
// Belege von Lexware laden und cachen
|
|
// --------------------------------------------------------
|
|
|
|
async fetchAndCacheVouchers(
|
|
tenantId: string,
|
|
lexwareContactId: string,
|
|
companyId?: string | null,
|
|
contactId?: string | null,
|
|
): Promise<number> {
|
|
this.logger.log(
|
|
`Lade Belege fuer Lexware-Kontakt ${lexwareContactId}...`,
|
|
);
|
|
|
|
// Alle relevanten Belegtypen laden
|
|
const voucherTypes = [
|
|
'invoice',
|
|
'quotation',
|
|
'orderconfirmation',
|
|
'creditnote',
|
|
];
|
|
let totalUpserted = 0;
|
|
|
|
for (const voucherType of voucherTypes) {
|
|
let page = 0;
|
|
let hasMore = true;
|
|
|
|
while (hasMore) {
|
|
const listResponse =
|
|
await this.lexwareClient.get<LexwareVoucherListResponse>(
|
|
'/v1/voucherlist',
|
|
{
|
|
contactId: lexwareContactId,
|
|
voucherType,
|
|
voucherStatus: 'any',
|
|
page,
|
|
size: 100,
|
|
},
|
|
);
|
|
|
|
for (const item of listResponse.content) {
|
|
try {
|
|
// Detail laden fuer vollstaendige Daten
|
|
const crmVoucherType = voucherTypeFromLexware(item.voucherType);
|
|
const endpoint = voucherTypeToLexwareEndpoint(crmVoucherType);
|
|
const detail =
|
|
await this.lexwareClient.get<LexwareVoucherDetail>(
|
|
`/v1/${endpoint}/${item.voucherId}`,
|
|
);
|
|
|
|
const cacheData = voucherDetailToCacheData(
|
|
detail,
|
|
crmVoucherType,
|
|
lexwareContactId,
|
|
);
|
|
|
|
// Upsert: Existierenden Beleg aktualisieren oder neuen anlegen
|
|
await this.prisma.lexwareVoucher.upsert({
|
|
where: {
|
|
tenantId_lexwareVoucherId: {
|
|
tenantId,
|
|
lexwareVoucherId: item.voucherId,
|
|
},
|
|
},
|
|
create: {
|
|
tenantId,
|
|
lexwareVoucherId: item.voucherId,
|
|
...cacheData,
|
|
companyId: companyId || undefined,
|
|
contactId: contactId || undefined,
|
|
fetchedAt: new Date(),
|
|
},
|
|
update: {
|
|
...cacheData,
|
|
companyId: companyId || undefined,
|
|
contactId: contactId || undefined,
|
|
fetchedAt: new Date(),
|
|
},
|
|
});
|
|
|
|
totalUpserted++;
|
|
} catch (error) {
|
|
this.logger.warn(
|
|
`Fehler beim Laden von Beleg ${item.voucherId}: ${error instanceof Error ? error.message : 'Unbekannt'}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
hasMore = !listResponse.last;
|
|
page++;
|
|
}
|
|
}
|
|
|
|
this.logger.log(
|
|
`${totalUpserted} Belege fuer Lexware-Kontakt ${lexwareContactId} gecacht.`,
|
|
);
|
|
return totalUpserted;
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Belege fuer Company abrufen (aus Cache)
|
|
// --------------------------------------------------------
|
|
|
|
async getVouchersForCompany(
|
|
tenantId: string,
|
|
companyId: string,
|
|
query: QueryLexwareVouchersDto,
|
|
) {
|
|
const company = await this.prisma.company.findFirst({
|
|
where: { id: companyId, tenantId },
|
|
});
|
|
if (!company) {
|
|
throw new NotFoundException('Unternehmen nicht gefunden');
|
|
}
|
|
|
|
return this.queryVouchers(tenantId, { companyId }, query);
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Belege fuer Contact abrufen (aus Cache)
|
|
// --------------------------------------------------------
|
|
|
|
async getVouchersForContact(
|
|
tenantId: string,
|
|
contactId: string,
|
|
query: QueryLexwareVouchersDto,
|
|
) {
|
|
const contact = await this.prisma.contact.findFirst({
|
|
where: { id: contactId, tenantId },
|
|
});
|
|
if (!contact) {
|
|
throw new NotFoundException('Kontakt nicht gefunden');
|
|
}
|
|
|
|
return this.queryVouchers(tenantId, { contactId }, query);
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Belege fuer Deal abrufen (ueber DealVoucher Join-Table)
|
|
// --------------------------------------------------------
|
|
|
|
async getVouchersForDeal(
|
|
tenantId: string,
|
|
dealId: string,
|
|
query: QueryLexwareVouchersDto,
|
|
) {
|
|
const deal = await this.prisma.deal.findFirst({
|
|
where: { id: dealId, tenantId },
|
|
});
|
|
if (!deal) {
|
|
throw new NotFoundException('Vorgang nicht gefunden');
|
|
}
|
|
|
|
const page = query.page ?? 1;
|
|
const pageSize = query.pageSize ?? 25;
|
|
|
|
const where: Prisma.DealVoucherWhereInput = {
|
|
tenantId,
|
|
dealId,
|
|
};
|
|
|
|
// Filter auf Voucher-Ebene
|
|
const voucherWhere: Prisma.LexwareVoucherWhereInput = {};
|
|
if (query.voucherType) {
|
|
voucherWhere.voucherType = query.voucherType;
|
|
}
|
|
if (query.voucherStatus) {
|
|
voucherWhere.voucherStatus = query.voucherStatus;
|
|
}
|
|
|
|
if (Object.keys(voucherWhere).length > 0) {
|
|
where.voucher = voucherWhere;
|
|
}
|
|
|
|
const [dealVouchers, total] = await Promise.all([
|
|
this.prisma.dealVoucher.findMany({
|
|
where,
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
orderBy: { linkedAt: 'desc' },
|
|
include: {
|
|
voucher: true,
|
|
},
|
|
}),
|
|
this.prisma.dealVoucher.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
data: dealVouchers.map((dv) => ({
|
|
...dv.voucher,
|
|
linkedBy: dv.linkedBy,
|
|
linkedAt: dv.linkedAt,
|
|
dealVoucherId: dv.id,
|
|
})),
|
|
total,
|
|
page,
|
|
pageSize,
|
|
};
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Beleg mit Deal verknuepfen
|
|
// --------------------------------------------------------
|
|
|
|
async linkVoucherToDeal(
|
|
tenantId: string,
|
|
dealId: string,
|
|
voucherId: string,
|
|
userId: string,
|
|
) {
|
|
// Deal pruefen
|
|
const deal = await this.prisma.deal.findFirst({
|
|
where: { id: dealId, tenantId },
|
|
});
|
|
if (!deal) {
|
|
throw new NotFoundException('Vorgang nicht gefunden');
|
|
}
|
|
|
|
// Voucher pruefen
|
|
const voucher = await this.prisma.lexwareVoucher.findFirst({
|
|
where: { id: voucherId, tenantId },
|
|
});
|
|
if (!voucher) {
|
|
throw new NotFoundException('Beleg nicht gefunden');
|
|
}
|
|
|
|
// Pruefe ob bereits verknuepft
|
|
const existing = await this.prisma.dealVoucher.findUnique({
|
|
where: { dealId_voucherId: { dealId, voucherId } },
|
|
});
|
|
if (existing) {
|
|
throw new ConflictException('Beleg ist bereits mit diesem Vorgang verknuepft');
|
|
}
|
|
|
|
return this.prisma.dealVoucher.create({
|
|
data: {
|
|
tenantId,
|
|
dealId,
|
|
voucherId,
|
|
linkedBy: userId,
|
|
},
|
|
include: {
|
|
voucher: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Beleg von Deal trennen
|
|
// --------------------------------------------------------
|
|
|
|
async unlinkVoucherFromDeal(
|
|
tenantId: string,
|
|
dealId: string,
|
|
voucherId: string,
|
|
) {
|
|
const dealVoucher = await this.prisma.dealVoucher.findFirst({
|
|
where: { tenantId, dealId, voucherId },
|
|
});
|
|
if (!dealVoucher) {
|
|
throw new NotFoundException('Verknuepfung nicht gefunden');
|
|
}
|
|
|
|
return this.prisma.dealVoucher.delete({
|
|
where: { id: dealVoucher.id },
|
|
});
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Cache manuell aktualisieren
|
|
// --------------------------------------------------------
|
|
|
|
async refreshCompanyVouchers(tenantId: string, companyId: string) {
|
|
const company = await this.prisma.company.findFirst({
|
|
where: { id: companyId, tenantId },
|
|
});
|
|
if (!company) {
|
|
throw new NotFoundException('Unternehmen nicht gefunden');
|
|
}
|
|
if (!company.lexwareContactId) {
|
|
throw new NotFoundException(
|
|
'Unternehmen ist nicht mit Lexware verknuepft',
|
|
);
|
|
}
|
|
|
|
return this.fetchAndCacheVouchers(
|
|
tenantId,
|
|
company.lexwareContactId,
|
|
companyId,
|
|
null,
|
|
);
|
|
}
|
|
|
|
async refreshContactVouchers(tenantId: string, contactId: string) {
|
|
const contact = await this.prisma.contact.findFirst({
|
|
where: { id: contactId, tenantId },
|
|
});
|
|
if (!contact) {
|
|
throw new NotFoundException('Kontakt nicht gefunden');
|
|
}
|
|
if (!contact.lexwareContactId) {
|
|
throw new NotFoundException('Kontakt ist nicht mit Lexware verknuepft');
|
|
}
|
|
|
|
return this.fetchAndCacheVouchers(
|
|
tenantId,
|
|
contact.lexwareContactId,
|
|
null,
|
|
contactId,
|
|
);
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Interne Hilfsfunktion: Paginierte Voucher-Abfrage
|
|
// --------------------------------------------------------
|
|
|
|
private async queryVouchers(
|
|
tenantId: string,
|
|
filter: { companyId?: string; contactId?: string },
|
|
query: QueryLexwareVouchersDto,
|
|
) {
|
|
const page = query.page ?? 1;
|
|
const pageSize = query.pageSize ?? 25;
|
|
|
|
const where: Prisma.LexwareVoucherWhereInput = {
|
|
tenantId,
|
|
...filter,
|
|
};
|
|
|
|
if (query.voucherType) {
|
|
where.voucherType = query.voucherType;
|
|
}
|
|
if (query.voucherStatus) {
|
|
where.voucherStatus = query.voucherStatus;
|
|
}
|
|
|
|
const [data, total] = await Promise.all([
|
|
this.prisma.lexwareVoucher.findMany({
|
|
where,
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
orderBy: { voucherDate: 'desc' },
|
|
}),
|
|
this.prisma.lexwareVoucher.count({ where }),
|
|
]);
|
|
|
|
return { data, total, page, pageSize };
|
|
}
|
|
}
|