[ 'method' => $method, 'timeout' => 20, 'ignore_errors' => true, 'header' => $headers, 'content' => $content, ], ]); $url = dolibarr_base_url() . '/api/index.php' . $path; $body = @file_get_contents($url, false, $context); $status = 0; foreach ($http_response_header ?? [] as $header) { if (preg_match('~^HTTP/\S+\s+(\d{3})~', $header, $matches) === 1) { $status = (int)$matches[1]; } } if ($body === false) { $error = error_get_last(); return ['ok' => false, 'status' => $status, 'data' => [], 'error' => $error['message'] ?? 'Unbekannter HTTP-Fehler']; } $decoded = json_decode($body, true); if ($status < 200 || $status >= 300) { $message = is_array($decoded) ? ($decoded['error']['message'] ?? $body) : $body; return ['ok' => false, 'status' => $status, 'data' => is_array($decoded) ? $decoded : [], 'error' => (string)$message]; } // Dolibarr's create-endpoints return a bare numeric id, not JSON. if (!is_array($decoded)) { return ['ok' => true, 'status' => $status, 'data' => ['id' => trim($body, "\" \n")], 'error' => null]; } return ['ok' => true, 'status' => $status, 'data' => $decoded, 'error' => null]; } /** * Resolves the Dolibarr thirdparty (customer) id for a tenant, creating it * on first use and caching it in tenant_billing.dolibarr_thirdparty_id. */ function dolibarr_find_or_create_thirdparty(PDO $pdo, int $tenantId): ?int { $billing = billing_fetch_or_init($pdo, $tenantId); if (!empty($billing['dolibarr_thirdparty_id'])) { return (int)$billing['dolibarr_thirdparty_id']; } $stmt = $pdo->prepare('SELECT name FROM tenants WHERE id = ?'); $stmt->execute([$tenantId]); $tenantName = (string)($stmt->fetchColumn() ?: "Mandant #{$tenantId}"); $stmt = $pdo->prepare( "SELECT u.email FROM users u INNER JOIN tenant_memberships tm ON tm.user_id = u.id WHERE tm.tenant_id = ? AND tm.role = 'owner' AND tm.status = 'active' ORDER BY tm.id ASC LIMIT 1" ); $stmt->execute([$tenantId]); $ownerEmail = $stmt->fetchColumn(); $result = dolibarr_request('POST', '/thirdparties', [ 'name' => "Kaffeeliste - {$tenantName}", 'client' => 1, 'code_client' => 'auto', 'email' => $ownerEmail !== false ? (string)$ownerEmail : '', 'note_private' => "Automatisch angelegt durch die Kaffeeliste-App fuer Mandant #{$tenantId}.", ]); if (!$result['ok']) { return null; } $thirdpartyId = (int)$result['data']['id']; billing_update($pdo, $tenantId, ['dolibarr_thirdparty_id' => (string)$thirdpartyId]); return $thirdpartyId; } /** * Creates a single-line invoice for a thirdparty and validates it * immediately (assigns the permanent, sequential invoice number). Intended * to be called only for genuine, already-collected Stripe payments. * * @return array{ok: bool, invoice_id: ?int, ref: ?string, error: ?string} */ function dolibarr_create_and_validate_invoice(int $thirdpartyId, string $description, int $amountCents): array { $created = dolibarr_request('POST', '/invoices', [ 'socid' => $thirdpartyId, 'type' => 0, 'note_private' => 'Automatisch erzeugt durch die Kaffeeliste-App aus einer erfolgreichen Stripe-Zahlung.', ]); if (!$created['ok']) { return ['ok' => false, 'invoice_id' => null, 'ref' => null, 'error' => 'Rechnung anlegen fehlgeschlagen: ' . $created['error']]; } $invoiceId = (int)$created['data']['id']; $line = dolibarr_request('POST', "/invoices/{$invoiceId}/lines", [ 'desc' => $description, 'qty' => 1, 'subprice' => round($amountCents / 100, 2), // Kleinunternehmer nach ยง 19 UStG: keine Umsatzsteuer ausgewiesen. 'tva_tx' => 0, ]); if (!$line['ok']) { return ['ok' => false, 'invoice_id' => $invoiceId, 'ref' => null, 'error' => 'Rechnungsposition anlegen fehlgeschlagen: ' . $line['error']]; } $validated = dolibarr_request('POST', "/invoices/{$invoiceId}/validate", []); if (!$validated['ok']) { return ['ok' => false, 'invoice_id' => $invoiceId, 'ref' => null, 'error' => 'Rechnung validieren fehlgeschlagen: ' . $validated['error']]; } $fetched = dolibarr_request('GET', "/invoices/{$invoiceId}"); $ref = $fetched['ok'] ? (string)($fetched['data']['ref'] ?? '') : null; return ['ok' => true, 'invoice_id' => $invoiceId, 'ref' => $ref, 'error' => null]; } /** * Orchestrates the Dolibarr side of a successful Stripe payment: resolves * (or creates) the tenant's Dolibarr customer, then creates and validates * a mirrored invoice. Stripe remains the sole payment channel; Dolibarr is * populated purely as a bookkeeping mirror, never invoked to collect money. * * @return array{ok: bool, invoice_id: ?int, ref: ?string, error: ?string} */ function dolibarr_sync_invoice_paid(PDO $pdo, int $tenantId, string $planCode, int $amountCents): array { $thirdpartyId = dolibarr_find_or_create_thirdparty($pdo, $tenantId); if ($thirdpartyId === null) { return ['ok' => false, 'invoice_id' => null, 'ref' => null, 'error' => 'Dolibarr-Kunde konnte nicht ermittelt/angelegt werden.']; } $plans = billing_plans(); $planLabel = $plans[$planCode]['label'] ?? $planCode; $description = "Kaffeeliste SaaS-Abo - Tarif {$planLabel} - " . date('m/Y'); return dolibarr_create_and_validate_invoice($thirdpartyId, $description, $amountCents); }