270 lines
8.0 KiB
PHP
270 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace OCA\Mitgliederverwaltung\Service;
|
|
|
|
use DateTime;
|
|
use OCA\Mitgliederverwaltung\Db\Family;
|
|
use OCA\Mitgliederverwaltung\Db\FamilyMapper;
|
|
use OCA\Mitgliederverwaltung\Db\MemberMapper;
|
|
use OCA\Mitgliederverwaltung\Validation\IbanValidator;
|
|
use OCP\AppFramework\Db\DoesNotExistException;
|
|
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
|
use OCP\DB\Exception;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
/**
|
|
* Service layer for family CRUD operations.
|
|
*
|
|
* Part of Issue #28.
|
|
*/
|
|
class FamilyService {
|
|
|
|
private FamilyMapper $familyMapper;
|
|
private MemberMapper $memberMapper;
|
|
private LoggerInterface $logger;
|
|
|
|
public function __construct(
|
|
FamilyMapper $familyMapper,
|
|
MemberMapper $memberMapper,
|
|
LoggerInterface $logger
|
|
) {
|
|
$this->familyMapper = $familyMapper;
|
|
$this->memberMapper = $memberMapper;
|
|
$this->logger = $logger;
|
|
}
|
|
|
|
/**
|
|
* Create a new family.
|
|
*
|
|
* @throws ValidationException
|
|
* @throws Exception
|
|
*/
|
|
public function create(array $data): array {
|
|
$this->validateRequiredFields($data);
|
|
$this->validateIban($data);
|
|
|
|
$now = (new DateTime())->format('Y-m-d H:i:s');
|
|
|
|
$family = new Family();
|
|
$family->setName(trim($data['name']));
|
|
$family->setKontoinhaberEncrypted($data['kontoinhaberEncrypted'] ?? null);
|
|
$family->setIbanEncrypted($data['ibanEncrypted'] ?? null);
|
|
$family->setBic($data['bic'] ?? null);
|
|
$family->setKreditinstitut($data['kreditinstitut'] ?? null);
|
|
$family->setCreatedAt($now);
|
|
$family->setUpdatedAt($now);
|
|
|
|
/** @var Family $family */
|
|
$family = $this->familyMapper->insert($family);
|
|
|
|
return $this->buildFamilyResponse($family);
|
|
}
|
|
|
|
/**
|
|
* Find a family by ID with linked members.
|
|
*
|
|
* @throws DoesNotExistException
|
|
* @throws MultipleObjectsReturnedException
|
|
* @throws Exception
|
|
*/
|
|
public function find(int $id): array {
|
|
$family = $this->familyMapper->findById($id);
|
|
return $this->buildFamilyResponse($family);
|
|
}
|
|
|
|
/**
|
|
* Find all families.
|
|
*
|
|
* @return array[]
|
|
* @throws Exception
|
|
*/
|
|
public function findAll(?int $limit = null, ?int $offset = null): array {
|
|
$families = $this->familyMapper->findAll($limit, $offset);
|
|
return array_map(fn(Family $f) => $this->buildFamilyResponse($f), $families);
|
|
}
|
|
|
|
/**
|
|
* Search families by name.
|
|
*
|
|
* @return array[]
|
|
* @throws Exception
|
|
*/
|
|
public function search(string $name): array {
|
|
$families = $this->familyMapper->findByName($name);
|
|
return array_map(fn(Family $f) => $this->buildFamilyResponse($f), $families);
|
|
}
|
|
|
|
/**
|
|
* Count all families.
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
public function countAll(): int {
|
|
return $this->familyMapper->countAll();
|
|
}
|
|
|
|
/**
|
|
* Update a family.
|
|
*
|
|
* @throws DoesNotExistException
|
|
* @throws MultipleObjectsReturnedException
|
|
* @throws ValidationException
|
|
* @throws Exception
|
|
*/
|
|
public function update(int $id, array $data): array {
|
|
$this->validateIban($data);
|
|
$family = $this->familyMapper->findById($id);
|
|
|
|
if (isset($data['name'])) {
|
|
if (trim($data['name']) === '') {
|
|
throw new ValidationException('Familienname darf nicht leer sein.');
|
|
}
|
|
$family->setName(trim($data['name']));
|
|
}
|
|
if (array_key_exists('kontoinhaberEncrypted', $data)) {
|
|
$family->setKontoinhaberEncrypted($data['kontoinhaberEncrypted']);
|
|
}
|
|
if (array_key_exists('ibanEncrypted', $data)) {
|
|
$family->setIbanEncrypted($data['ibanEncrypted']);
|
|
}
|
|
if (array_key_exists('bic', $data)) {
|
|
$family->setBic($data['bic']);
|
|
}
|
|
if (array_key_exists('kreditinstitut', $data)) {
|
|
$family->setKreditinstitut($data['kreditinstitut']);
|
|
}
|
|
|
|
$family->setUpdatedAt((new DateTime())->format('Y-m-d H:i:s'));
|
|
|
|
/** @var Family $family */
|
|
$family = $this->familyMapper->update($family);
|
|
|
|
return $this->buildFamilyResponse($family);
|
|
}
|
|
|
|
/**
|
|
* Delete a family. Unlinks all members first.
|
|
*
|
|
* @throws DoesNotExistException
|
|
* @throws MultipleObjectsReturnedException
|
|
* @throws Exception
|
|
*/
|
|
public function delete(int $id): void {
|
|
$family = $this->familyMapper->findById($id);
|
|
|
|
// Unlink all members from this family
|
|
$members = $this->memberMapper->findByFamily($id);
|
|
foreach ($members as $member) {
|
|
$member->setFamilyId(null);
|
|
$this->memberMapper->update($member);
|
|
}
|
|
|
|
$this->familyMapper->delete($family);
|
|
|
|
$this->logger->info('Family deleted', [
|
|
'familyId' => $id,
|
|
'app' => 'mitgliederverwaltung',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Link a member to a family.
|
|
*
|
|
* @throws DoesNotExistException
|
|
* @throws MultipleObjectsReturnedException
|
|
* @throws Exception
|
|
*/
|
|
public function linkMember(int $familyId, int $memberId): void {
|
|
// Verify family exists
|
|
$this->familyMapper->findById($familyId);
|
|
|
|
$member = $this->memberMapper->findById($memberId);
|
|
$member->setFamilyId($familyId);
|
|
$member->setUpdatedAt((new DateTime())->format('Y-m-d H:i:s'));
|
|
$this->memberMapper->update($member);
|
|
}
|
|
|
|
/**
|
|
* Unlink a member from a family.
|
|
*
|
|
* @throws DoesNotExistException
|
|
* @throws MultipleObjectsReturnedException
|
|
* @throws Exception
|
|
*/
|
|
public function unlinkMember(int $familyId, int $memberId): void {
|
|
$member = $this->memberMapper->findById($memberId);
|
|
|
|
if ($member->getFamilyId() !== $familyId) {
|
|
throw new ValidationException('Mitglied gehoert nicht zu dieser Familie.');
|
|
}
|
|
|
|
$member->setFamilyId(null);
|
|
$member->setUpdatedAt((new DateTime())->format('Y-m-d H:i:s'));
|
|
$this->memberMapper->update($member);
|
|
}
|
|
|
|
/**
|
|
* Get the count of active children in a family.
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
public function getActiveChildrenCount(int $familyId): int {
|
|
$members = $this->memberMapper->findByFamily($familyId);
|
|
return count(array_filter($members, function ($member) {
|
|
return $member->getStatus() === 'aktiv' && $member->getRolle() === 'mitglied';
|
|
}));
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* @throws ValidationException
|
|
*/
|
|
private function validateRequiredFields(array $data): void {
|
|
if (!isset($data['name']) || trim($data['name']) === '') {
|
|
throw new ValidationException('Familienname ist erforderlich.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate IBAN if provided and non-empty.
|
|
*
|
|
* @throws ValidationException
|
|
*/
|
|
private function validateIban(array $data): void {
|
|
$iban = $data['ibanEncrypted'] ?? null;
|
|
if ($iban !== null && trim($iban) !== '') {
|
|
$error = IbanValidator::getError($iban);
|
|
if ($error !== null) {
|
|
throw new ValidationException($error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build response array with linked members.
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
private function buildFamilyResponse(Family $family): array {
|
|
$result = $family->jsonSerialize();
|
|
$members = $this->memberMapper->findByFamily($family->getId());
|
|
|
|
$result['members'] = array_map(fn($m) => [
|
|
'id' => $m->getId(),
|
|
'vorname' => $m->getVorname(),
|
|
'nachname' => $m->getNachname(),
|
|
'rolle' => $m->getRolle(),
|
|
'status' => $m->getStatus(),
|
|
], $members);
|
|
|
|
$result['activeChildrenCount'] = count(array_filter($members, function ($m) {
|
|
return $m->getStatus() === 'aktiv' && $m->getRolle() === 'mitglied';
|
|
}));
|
|
|
|
return $result;
|
|
}
|
|
}
|