mirror of
https://github.com/chenasraf/cospend-nc.git
synced 2026-05-17 17:38:10 +00:00
1061 lines
35 KiB
PHP
1061 lines
35 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Nextcloud - Cospend
|
|
*
|
|
* This file is licensed under the Affero General Public License version 3 or
|
|
* later. See the COPYING file.
|
|
*
|
|
* @author Julien Veyssier
|
|
* @copyright Julien Veyssier 2024
|
|
*/
|
|
|
|
namespace OCA\Cospend\Service;
|
|
|
|
use DateTime;
|
|
use Generator;
|
|
use OC\User\NoUserException;
|
|
use OCA\Cospend\AppInfo\Application;
|
|
use OCA\Cospend\Db\Invitation;
|
|
use OCA\Cospend\Db\InvitationMapper;
|
|
use OCA\Cospend\Utils;
|
|
|
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
|
use OCP\Files\File;
|
|
use OCP\Files\Folder;
|
|
use OCP\Files\InvalidPathException;
|
|
use OCP\Files\IRootFolder;
|
|
use OCP\Files\NotFoundException;
|
|
use OCP\Files\NotPermittedException;
|
|
use OCP\IConfig;
|
|
use OCP\IDBConnection;
|
|
use OCP\IL10N;
|
|
use OCP\IUserManager;
|
|
use OCP\Lock\LockedException;
|
|
use Throwable;
|
|
|
|
class CospendService {
|
|
|
|
public function __construct(
|
|
private LocalProjectService $localProjectService,
|
|
private InvitationMapper $invitationMapper,
|
|
private IRootFolder $root,
|
|
private IL10N $l10n,
|
|
private IUserManager $userManager,
|
|
private IDbConnection $db,
|
|
private IConfig $config,
|
|
) {
|
|
}
|
|
|
|
public function getFederatedProjects(string $userId): array {
|
|
$invitations = $this->invitationMapper->getInvitationsForUser($userId, Invitation::STATE_ACCEPTED);
|
|
return array_map(static function (Invitation $invitation) {
|
|
return $invitation->getRemoteProjectId() . '@' . $invitation->getRemoteServerUrl();
|
|
}, $invitations);
|
|
}
|
|
|
|
/**
|
|
* Wrap the import process in an atomic DB transaction
|
|
* This increases insert performance a lot
|
|
*
|
|
* importCsvProject() still takes care of cleaning up created entities in case of error
|
|
* but this could be done by rollBack
|
|
*
|
|
* This could be done with TTransactional::atomic() when we drop support for NC < 24
|
|
*
|
|
* @param $handle
|
|
* @param string $userId
|
|
* @param string $projectName
|
|
* @return array
|
|
* @throws Throwable
|
|
* @throws \OCP\DB\Exception
|
|
*/
|
|
public function importCsvProjectAtomicWrapper($handle, string $userId, string $projectName): array {
|
|
$this->db->beginTransaction();
|
|
try {
|
|
$result = $this->importCsvProjectStream($handle, $userId, $projectName);
|
|
$this->db->commit();
|
|
return $result;
|
|
} catch (Throwable $e) {
|
|
$this->db->rollBack();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Import CSV project file
|
|
*
|
|
* @param string $path
|
|
* @param string $userId
|
|
* @return array
|
|
* @throws NoUserException
|
|
* @throws NotFoundException
|
|
* @throws NotPermittedException
|
|
* @throws Throwable
|
|
* @throws \OCP\DB\Exception
|
|
*/
|
|
public function importCsvProject(string $path, string $userId): array {
|
|
$cleanPath = str_replace(['../', '..\\'], '', $path);
|
|
$userFolder = $this->root->getUserFolder($userId);
|
|
if ($userFolder->nodeExists($cleanPath)) {
|
|
$file = $userFolder->get($cleanPath);
|
|
if ($file instanceof File) {
|
|
if (($handle = $file->fopen('r')) !== false) {
|
|
$projectName = preg_replace('/\.csv$/', '', $file->getName());
|
|
return $this->importCsvProjectAtomicWrapper($handle, $userId, $projectName);
|
|
} else {
|
|
return ['message' => $this->l10n->t('Access denied')];
|
|
}
|
|
} else {
|
|
return ['message' => $this->l10n->t('Access denied')];
|
|
}
|
|
} else {
|
|
return ['message' => $this->l10n->t('Access denied')];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $handle
|
|
* @param string $userId
|
|
* @param string $projectName
|
|
* @return array
|
|
* @throws \OCP\DB\Exception
|
|
*/
|
|
public function importCsvProjectStream($handle, string $userId, string $projectName): array {
|
|
$columns = [];
|
|
$membersByName = [];
|
|
$bills = [];
|
|
$currencies = [];
|
|
$mainCurrencyName = null;
|
|
$categories = [];
|
|
$categoryIdConv = [];
|
|
$paymentModes = [];
|
|
$paymentModeIdConv = [];
|
|
$previousLineEmpty = false;
|
|
$currentSection = null;
|
|
$row = 0;
|
|
while (($data = fgetcsv($handle, 0, ',')) !== false) {
|
|
$uni = array_unique($data);
|
|
if ($data === [null] || (count($uni) === 1 && $uni[0] === '')) {
|
|
$previousLineEmpty = true;
|
|
} elseif ($row === 0 || $previousLineEmpty) {
|
|
// determine which section we're entering
|
|
$previousLineEmpty = false;
|
|
$nbCol = count($data);
|
|
$columns = [];
|
|
for ($c = 0; $c < $nbCol; $c++) {
|
|
if ($data[$c] !== '') {
|
|
$columns[$data[$c]] = $c;
|
|
}
|
|
}
|
|
if (array_key_exists('what', $columns)
|
|
&& array_key_exists('amount', $columns)
|
|
&& (array_key_exists('date', $columns) || array_key_exists('timestamp', $columns))
|
|
&& array_key_exists('payer_name', $columns)
|
|
&& array_key_exists('payer_weight', $columns)
|
|
&& array_key_exists('owers', $columns)
|
|
) {
|
|
$currentSection = 'bills';
|
|
} elseif (array_key_exists('name', $columns)
|
|
&& array_key_exists('weight', $columns)
|
|
&& array_key_exists('active', $columns)
|
|
&& array_key_exists('color', $columns)
|
|
) {
|
|
$currentSection = 'members';
|
|
} elseif (array_key_exists('icon', $columns)
|
|
&& array_key_exists('color', $columns)
|
|
&& array_key_exists('paymentmodeid', $columns)
|
|
&& array_key_exists('paymentmodename', $columns)
|
|
) {
|
|
$currentSection = 'paymentmodes';
|
|
} elseif (array_key_exists('icon', $columns)
|
|
&& array_key_exists('color', $columns)
|
|
&& array_key_exists('categoryid', $columns)
|
|
&& array_key_exists('categoryname', $columns)
|
|
) {
|
|
$currentSection = 'categories';
|
|
} elseif (array_key_exists('exchange_rate', $columns)
|
|
&& array_key_exists('currencyname', $columns)
|
|
) {
|
|
$currentSection = 'currencies';
|
|
} else {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Malformed CSV, bad column names at line %1$s', [$row + 1])];
|
|
}
|
|
} else {
|
|
// normal line: bill/category/payment mode/currency
|
|
$previousLineEmpty = false;
|
|
if ($currentSection === 'categories') {
|
|
if (mb_strlen($data[$columns['icon']], 'UTF-8') && preg_match('!\S!u', $data[$columns['icon']])) {
|
|
$icon = $data[$columns['icon']];
|
|
} else {
|
|
$icon = null;
|
|
}
|
|
$color = $data[$columns['color']];
|
|
$categoryname = $data[$columns['categoryname']];
|
|
if (!is_numeric($data[$columns['categoryid']])) {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Error when adding category %1$s', [$categoryname])];
|
|
}
|
|
$categoryid = (int) $data[$columns['categoryid']];
|
|
$categories[] = [
|
|
'icon' => $icon,
|
|
'color' => $color,
|
|
'id' => $categoryid,
|
|
'name' => $categoryname,
|
|
];
|
|
} elseif ($currentSection === 'paymentmodes') {
|
|
if (mb_strlen($data[$columns['icon']], 'UTF-8') && preg_match('!\S!u', $data[$columns['icon']])) {
|
|
$icon = $data[$columns['icon']];
|
|
} else {
|
|
$icon = null;
|
|
}
|
|
$paymentmodename = $data[$columns['paymentmodename']];
|
|
if (!is_numeric($data[$columns['paymentmodeid']])) {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Error when adding payment mode %1$s', [$paymentmodename])];
|
|
}
|
|
$color = $data[$columns['color']];
|
|
$paymentmodeid = (int) $data[$columns['paymentmodeid']];
|
|
$paymentModes[] = [
|
|
'icon' => $icon,
|
|
'color' => $color,
|
|
'id' => $paymentmodeid,
|
|
'name' => $paymentmodename,
|
|
];
|
|
} elseif ($currentSection === 'currencies') {
|
|
$name = $data[$columns['currencyname']];
|
|
if (!is_numeric($data[$columns['exchange_rate']])) {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Error when adding currency %1$s', [$name])];
|
|
}
|
|
$exchange_rate = (float) $data[$columns['exchange_rate']];
|
|
if (($exchange_rate) === 1.0) {
|
|
$mainCurrencyName = $name;
|
|
} else {
|
|
$currencies[] = [
|
|
'name' => $name,
|
|
'exchange_rate' => $exchange_rate,
|
|
];
|
|
}
|
|
} elseif ($currentSection === 'members') {
|
|
$name = trim($data[$columns['name']]);
|
|
if (!is_numeric($data[$columns['weight']]) || !is_numeric($data[$columns['active']])) {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Error when adding member %1$s', [$name])];
|
|
}
|
|
$weight = (float) $data[$columns['weight']];
|
|
$active = (int) $data[$columns['active']];
|
|
$color = $data[$columns['color']];
|
|
if (strlen($name) > 0
|
|
&& preg_match('/^#[0-9A-Fa-f]+$/', $color) !== false
|
|
) {
|
|
$membersByName[$name] = [
|
|
'weight' => $weight,
|
|
'active' => $active !== 0,
|
|
'color' => $color,
|
|
];
|
|
} else {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Malformed CSV, invalid member on line %1$s', [$row + 1])];
|
|
}
|
|
} elseif ($currentSection === 'bills') {
|
|
$what = $data[$columns['what']];
|
|
if (!is_numeric($data[$columns['amount']])) {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Malformed CSV, invalid amount on line %1$s', [$row + 1])];
|
|
}
|
|
$amount = (float) $data[$columns['amount']];
|
|
$timestamp = null;
|
|
// priority to timestamp
|
|
if (array_key_exists('timestamp', $columns)) {
|
|
$timestamp = (int) $data[$columns['timestamp']];
|
|
} elseif (array_key_exists('date', $columns)) {
|
|
$date = $data[$columns['date']];
|
|
$datetime = DateTime::createFromFormat('Y-m-d', $date);
|
|
if ($datetime !== false) {
|
|
$timestamp = $datetime->getTimestamp();
|
|
}
|
|
}
|
|
if ($timestamp === null) {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Malformed CSV, missing or invalid date/timestamp on line %1$s', [$row + 1])];
|
|
}
|
|
$payer_name = $data[$columns['payer_name']];
|
|
$payer_weight = $data[$columns['payer_weight']];
|
|
$owers = $data[$columns['owers']];
|
|
$payer_active = array_key_exists('payer_active', $columns) ? $data[$columns['payer_active']] : 1;
|
|
$repeat = array_key_exists('repeat', $columns) ? $data[$columns['repeat']] : Application::FREQUENCY_NO;
|
|
$categoryid = array_key_exists('categoryid', $columns) ? (int) $data[$columns['categoryid']] : null;
|
|
$paymentmode = array_key_exists('paymentmode', $columns) ? $data[$columns['paymentmode']] : null;
|
|
$paymentmodeid = array_key_exists('paymentmodeid', $columns) ? (int) $data[$columns['paymentmodeid']] : null;
|
|
$repeatallactive = array_key_exists('repeatallactive', $columns) ? (int) $data[$columns['repeatallactive']] : 0;
|
|
$repeatuntil = array_key_exists('repeatuntil', $columns) ? $data[$columns['repeatuntil']] : null;
|
|
$repeatfreq = array_key_exists('repeatfreq', $columns) ? (int) $data[$columns['repeatfreq']] : 1;
|
|
$comment = array_key_exists('comment', $columns) ? urldecode($data[$columns['comment']] ?? '') : null;
|
|
$deleted = array_key_exists('deleted', $columns) ? (int) $data[$columns['deleted']] : 0;
|
|
|
|
// manage members
|
|
if (!isset($membersByName[$payer_name])) {
|
|
$membersByName[$payer_name] = [
|
|
'active' => ((int) $payer_active) !== 0,
|
|
'weight' => 1.0,
|
|
'color' => null,
|
|
];
|
|
if (is_numeric($payer_weight)) {
|
|
$membersByName[$payer_name]['weight'] = (float) $payer_weight;
|
|
} else {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Malformed CSV, invalid payer weight on line %1$s', [$row + 1])];
|
|
}
|
|
}
|
|
if (strlen($owers) === 0) {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Malformed CSV, invalid owers on line %1$s', [$row + 1])];
|
|
}
|
|
if ($what !== 'deleteMeIfYouWant') {
|
|
$owersArray = explode(',', $owers);
|
|
foreach ($owersArray as $ower) {
|
|
$strippedOwer = trim($ower);
|
|
if (strlen($strippedOwer) === 0) {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Malformed CSV, invalid owers on line %1$s', [$row + 1])];
|
|
}
|
|
if (!isset($membersByName[$strippedOwer])) {
|
|
$membersByName[$strippedOwer]['weight'] = 1.0;
|
|
$membersByName[$strippedOwer]['active'] = true;
|
|
$membersByName[$strippedOwer]['color'] = null;
|
|
}
|
|
}
|
|
$bills[] = [
|
|
'what' => $what,
|
|
'comment' => $comment,
|
|
'timestamp' => $timestamp,
|
|
'amount' => $amount,
|
|
'payer_name' => $payer_name,
|
|
'owers' => $owersArray,
|
|
'paymentmode' => $paymentmode,
|
|
'paymentmodeid' => $paymentmodeid,
|
|
'categoryid' => $categoryid,
|
|
'repeat' => $repeat,
|
|
'repeatuntil' => $repeatuntil,
|
|
'repeatallactive' => $repeatallactive,
|
|
'repeatfreq' => $repeatfreq,
|
|
'deleted' => $deleted,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
$row++;
|
|
}
|
|
fclose($handle);
|
|
|
|
$memberNameToId = [];
|
|
|
|
// add project
|
|
$user = $this->userManager->get($userId);
|
|
$userEmail = $user->getEMailAddress();
|
|
$projectid = Utils::slugify($projectName);
|
|
$createDefaultCategories = (count($categories) === 0);
|
|
$createDefaultPaymentModes = (count($paymentModes) === 0);
|
|
$projResult = $this->localProjectService->createProject(
|
|
$projectName, $projectid, $userEmail, $userId,
|
|
$createDefaultCategories, $createDefaultPaymentModes
|
|
);
|
|
if (!isset($projResult['id'])) {
|
|
return ['message' => $this->l10n->t('Error in project creation, %1$s', [$projResult['message'] ?? ''])];
|
|
}
|
|
// set project main currency
|
|
if ($mainCurrencyName !== null) {
|
|
$this->localProjectService->editProject($projectid, $projectName, null, null, $mainCurrencyName);
|
|
}
|
|
// add payment modes
|
|
foreach ($paymentModes as $pm) {
|
|
$insertedPmId = $this->localProjectService->createPaymentMode($projectid, $pm['name'], $pm['icon'], $pm['color']);
|
|
$paymentModeIdConv[$pm['id']] = $insertedPmId;
|
|
}
|
|
// add categories
|
|
foreach ($categories as $cat) {
|
|
$insertedCatId = $this->localProjectService->createCategory($projectid, $cat['name'], $cat['icon'], $cat['color']);
|
|
$categoryIdConv[$cat['id']] = $insertedCatId;
|
|
}
|
|
// add currencies
|
|
foreach ($currencies as $cur) {
|
|
$insertedCurId = $this->localProjectService->createCurrency($projectid, $cur['name'], $cur['exchange_rate']);
|
|
}
|
|
// add members
|
|
foreach ($membersByName as $memberName => $member) {
|
|
try {
|
|
$insertedMember = $this->localProjectService->createMember(
|
|
$projectid, $memberName, $member['weight'], $member['active'], $member['color'] ?? null
|
|
);
|
|
} catch (\Throwable $e) {
|
|
$this->localProjectService->deleteProject($projectid);
|
|
return ['message' => $this->l10n->t('Error when adding member %1$s', [$memberName])];
|
|
}
|
|
$memberNameToId[$memberName] = $insertedMember['id'];
|
|
}
|
|
// add bills
|
|
foreach ($bills as $bill) {
|
|
// manage category id if this is a custom category
|
|
$catId = $bill['categoryid'];
|
|
if ($catId !== null && $catId > 0) {
|
|
$catId = $categoryIdConv[$catId];
|
|
}
|
|
// manage payment mode id if this is a custom payment mode
|
|
$pmId = $bill['paymentmodeid'];
|
|
if ($pmId !== null && $pmId > 0) {
|
|
$pmId = $paymentModeIdConv[$pmId];
|
|
}
|
|
$payerId = $memberNameToId[$bill['payer_name']];
|
|
$owerIds = [];
|
|
foreach ($bill['owers'] as $owerName) {
|
|
$strippedOwer = trim($owerName);
|
|
$owerIds[] = $memberNameToId[$strippedOwer];
|
|
}
|
|
$owerIdsStr = implode(',', $owerIds);
|
|
try {
|
|
$this->localProjectService->createBill(
|
|
$projectid, null, $bill['what'], $payerId,
|
|
$owerIdsStr, $bill['amount'], $bill['repeat'],
|
|
$bill['paymentmode'], $pmId,
|
|
$catId, $bill['repeatallactive'],
|
|
$bill['repeatuntil'], $bill['timestamp'], $bill['comment'], $bill['repeatfreq'],
|
|
$bill['deleted'] ?? 0
|
|
);
|
|
} catch (\Throwable $e) {
|
|
$this->localProjectService->deleteProject($projectid);
|
|
return ['message' => $this->l10n->t('Error when adding bill %1$s', [$bill['what']])];
|
|
}
|
|
}
|
|
return ['project_id' => $projectid];
|
|
}
|
|
|
|
/**
|
|
* Import SplitWise project file
|
|
*
|
|
* @param string $path
|
|
* @param string $userId
|
|
* @return array
|
|
* @throws NoUserException
|
|
* @throws NotFoundException
|
|
* @throws NotPermittedException
|
|
* @throws \OCP\DB\Exception
|
|
*/
|
|
public function importSWProject(string $path, string $userId): array {
|
|
$cleanPath = str_replace(['../', '..\\'], '', $path);
|
|
$userFolder = $this->root->getUserFolder($userId);
|
|
if ($userFolder->nodeExists($cleanPath)) {
|
|
$file = $userFolder->get($cleanPath);
|
|
if ($file instanceof File) {
|
|
if (($handle = $file->fopen('r')) !== false) {
|
|
$columns = [];
|
|
$membersWeight = [];
|
|
$bills = [];
|
|
$owersArray = [];
|
|
$categoryNames = [];
|
|
$row = 0;
|
|
$nbCol = 0;
|
|
|
|
$columnNamesLineFound = false;
|
|
while (($data = fgetcsv($handle, 1000, ',')) !== false) {
|
|
// look for column order line
|
|
if (!$columnNamesLineFound) {
|
|
$nbCol = count($data);
|
|
for ($c = 0; $c < $nbCol; $c++) {
|
|
$columns[$data[$c]] = $c;
|
|
}
|
|
if (!array_key_exists('Date', $columns)
|
|
|| !array_key_exists('Description', $columns)
|
|
|| !array_key_exists('Category', $columns)
|
|
|| !array_key_exists('Cost', $columns)
|
|
|| !array_key_exists('Currency', $columns)
|
|
) {
|
|
$columns = [];
|
|
$row++;
|
|
continue;
|
|
}
|
|
$columnNamesLineFound = true;
|
|
// manage members
|
|
$m = 0;
|
|
for ($c = 5; $c < $nbCol; $c++) {
|
|
$owersArray[$m] = $data[$c];
|
|
$m++;
|
|
}
|
|
foreach ($owersArray as $ower) {
|
|
if (strlen($ower) === 0) {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Malformed CSV, cannot have an empty ower')];
|
|
}
|
|
if (!array_key_exists($ower, $membersWeight)) {
|
|
$membersWeight[$ower] = 1.0;
|
|
}
|
|
}
|
|
} elseif (!isset($data[$columns['Date']]) || empty($data[$columns['Date']])) {
|
|
// skip empty lines
|
|
} elseif (isset($data[$columns['Description']]) && $data[$columns['Description']] === 'Total balance') {
|
|
// skip the total lines
|
|
} else {
|
|
// normal line : bill
|
|
$what = $data[$columns['Description']];
|
|
$cost = trim($data[$columns['Cost']]);
|
|
if (empty($cost)) {
|
|
// skip lines with no cost, it might be the balances line
|
|
$row++;
|
|
continue;
|
|
}
|
|
$date = $data[$columns['Date']];
|
|
$datetime = DateTime::createFromFormat('Y-m-d', $date);
|
|
if ($datetime === false) {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Malformed CSV, missing or invalid date/timestamp on line %1$s', [$row])];
|
|
}
|
|
$timestamp = $datetime->getTimestamp();
|
|
|
|
$categoryName = null;
|
|
// manage categories
|
|
if (array_key_exists('Category', $columns)
|
|
&& $data[$columns['Category']] !== null
|
|
&& $data[$columns['Category']] !== '') {
|
|
$categoryName = $data[$columns['Category']];
|
|
if (!in_array($categoryName, $categoryNames)) {
|
|
$categoryNames[] = $categoryName;
|
|
}
|
|
}
|
|
|
|
// new algorithm
|
|
// get those with a negative value, they will be the owers in generated bills
|
|
$negativeCols = [];
|
|
for ($c = 5; $c < $nbCol; $c++) {
|
|
if (!is_numeric($data[$c])) {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Malformed CSV, bad amount on line %1$s', [$row])];
|
|
}
|
|
$amount = (float) $data[$c];
|
|
if ($amount < 0) {
|
|
$negativeCols[] = $c;
|
|
}
|
|
}
|
|
$owersList = array_map(static function ($c) use ($owersArray) {
|
|
return $owersArray[$c - 5];
|
|
}, $negativeCols);
|
|
// each positive one: bill with member-specific amount (not the full amount), owers are the negative ones
|
|
for ($c = 5; $c < $nbCol; $c++) {
|
|
$amount = (float) $data[$c];
|
|
if ($amount > 0) {
|
|
$payer_name = $owersArray[$c - 5];
|
|
if (empty($payer_name)) {
|
|
fclose($handle);
|
|
return ['message' => $this->l10n->t('Malformed CSV, no payer on line %1$s', [$row])];
|
|
}
|
|
$bill = [
|
|
'what' => $what,
|
|
'timestamp' => $timestamp,
|
|
'amount' => $amount,
|
|
'payer_name' => $payer_name,
|
|
'owers' => $owersList
|
|
];
|
|
if ($categoryName !== null) {
|
|
$bill['category_name'] = $categoryName;
|
|
}
|
|
$bills[] = $bill;
|
|
}
|
|
}
|
|
}
|
|
$row++;
|
|
}
|
|
fclose($handle);
|
|
|
|
if (!$columnNamesLineFound) {
|
|
return ['message' => $this->l10n->t('Malformed CSV, impossible to find the column names. Make sure your Splitwise account language is set to English first, then export the project again.')];
|
|
}
|
|
|
|
$memberNameToId = [];
|
|
|
|
// add project
|
|
$user = $this->userManager->get($userId);
|
|
$userEmail = $user->getEMailAddress();
|
|
$projectName = preg_replace('/\.csv$/', '', $file->getName());
|
|
$projectid = Utils::slugify($projectName);
|
|
// create default categories only if none are found in the CSV
|
|
$createDefaultCategories = (count($categoryNames) === 0);
|
|
$projResult = $this->localProjectService->createProject(
|
|
$projectName, $projectid, $userEmail,
|
|
$userId, $createDefaultCategories
|
|
);
|
|
if (!isset($projResult['id'])) {
|
|
return ['message' => $this->l10n->t('Error in project creation, %1$s', [$projResult['message'] ?? ''])];
|
|
}
|
|
// add categories
|
|
$catNameToId = [];
|
|
foreach ($categoryNames as $categoryName) {
|
|
$insertedCatId = $this->localProjectService->createCategory($projectid, $categoryName, null, '#000000');
|
|
/*
|
|
if (!is_numeric($insertedCatId)) {
|
|
$this->deleteProject($projectid);
|
|
return ['message' => $this->l10n->t('Error when adding category %1$s', [$categoryName])];
|
|
}
|
|
*/
|
|
$catNameToId[$categoryName] = $insertedCatId;
|
|
}
|
|
// add members
|
|
foreach ($membersWeight as $memberName => $weight) {
|
|
try {
|
|
$insertedMember = $this->localProjectService->createMember($projectid, $memberName, $weight);
|
|
} catch (\Throwable $e) {
|
|
$this->localProjectService->deleteProject($projectid);
|
|
return ['message' => $this->l10n->t('Error when adding member %1$s', [$memberName])];
|
|
}
|
|
$memberNameToId[$memberName] = $insertedMember['id'];
|
|
}
|
|
// add bills
|
|
foreach ($bills as $bill) {
|
|
$payerId = $memberNameToId[$bill['payer_name']];
|
|
$owerIds = [];
|
|
foreach ($bill['owers'] as $owerName) {
|
|
$owerIds[] = $memberNameToId[$owerName];
|
|
}
|
|
$owerIdsStr = implode(',', $owerIds);
|
|
// category
|
|
$catId = null;
|
|
if (array_key_exists('category_name', $bill)
|
|
&& array_key_exists($bill['category_name'], $catNameToId)) {
|
|
$catId = $catNameToId[$bill['category_name']];
|
|
}
|
|
try {
|
|
$this->localProjectService->createBill(
|
|
$projectid, null, $bill['what'], $payerId, $owerIdsStr,
|
|
$bill['amount'], Application::FREQUENCY_NO, null, 0, $catId,
|
|
0, null, $bill['timestamp'], null, null
|
|
);
|
|
} catch (\Throwable $e) {
|
|
$this->localProjectService->deleteProject($projectid);
|
|
return ['message' => $this->l10n->t('Error when adding bill %1$s', [$bill['what']])];
|
|
}
|
|
}
|
|
return ['project_id' => $projectid];
|
|
} else {
|
|
return ['message' => $this->l10n->t('Access denied')];
|
|
}
|
|
} else {
|
|
return ['message' => $this->l10n->t('Access denied')];
|
|
}
|
|
} else {
|
|
return ['message' => $this->l10n->t('Access denied')];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* auto export
|
|
* triggered by NC cron job
|
|
*
|
|
* export projects
|
|
*/
|
|
public function cronAutoExport(): void {
|
|
date_default_timezone_set('UTC');
|
|
// last day
|
|
$now = new DateTime();
|
|
$y = $now->format('Y');
|
|
$m = $now->format('m');
|
|
$d = $now->format('d');
|
|
|
|
// get begining of today
|
|
$dateMaxDay = new DateTime($y . '-' . $m . '-' . $d);
|
|
$maxDayTimestamp = $dateMaxDay->getTimestamp();
|
|
$minDayTimestamp = $maxDayTimestamp - (24 * 60 * 60);
|
|
|
|
$dateMaxDay->modify('-1 day');
|
|
$dailySuffix = '_'.$this->l10n->t('daily').'_'.$dateMaxDay->format('Y-m-d');
|
|
|
|
// last week
|
|
$now = new DateTime();
|
|
while (((int) $now->format('N')) !== 1) {
|
|
$now->modify('-1 day');
|
|
}
|
|
$y = $now->format('Y');
|
|
$m = $now->format('m');
|
|
$d = $now->format('d');
|
|
$dateWeekMax = new DateTime($y.'-'.$m.'-'.$d);
|
|
$maxWeekTimestamp = $dateWeekMax->getTimestamp();
|
|
$minWeekTimestamp = $maxWeekTimestamp - (7 * 24 * 60 * 60);
|
|
$dateWeekMin = new DateTime($y.'-'.$m.'-'.$d);
|
|
$dateWeekMin->modify('-7 day');
|
|
$weeklySuffix = '_'.$this->l10n->t('weekly').'_'.$dateWeekMin->format('Y-m-d');
|
|
|
|
// last month
|
|
$now = new DateTime();
|
|
while (((int) $now->format('d')) !== 1) {
|
|
$now->modify('-1 day');
|
|
}
|
|
$y = $now->format('Y');
|
|
$m = $now->format('m');
|
|
$d = $now->format('d');
|
|
$dateMonthMax = new DateTime($y.'-'.$m.'-'.$d);
|
|
$maxMonthTimestamp = $dateMonthMax->getTimestamp();
|
|
$now->modify('-1 day');
|
|
while (((int) $now->format('d')) !== 1) {
|
|
$now->modify('-1 day');
|
|
}
|
|
$y = (int) $now->format('Y');
|
|
$m = (int) $now->format('m');
|
|
$d = (int) $now->format('d');
|
|
$dateMonthMin = new DateTime($y.'-'.$m.'-'.$d);
|
|
$minMonthTimestamp = $dateMonthMin->getTimestamp();
|
|
$monthlySuffix = '_'.$this->l10n->t('monthly').'_'.$dateMonthMin->format('Y-m');
|
|
|
|
// $weekFilterArray = [];
|
|
// $weekFilterArray['tsmin'] = $minWeekTimestamp;
|
|
// $weekFilterArray['tsmax'] = $maxWeekTimestamp;
|
|
// $dayFilterArray = [];
|
|
// $dayFilterArray['tsmin'] = $minDayTimestamp;
|
|
// $dayFilterArray['tsmax'] = $maxDayTimestamp;
|
|
// $monthFilterArray = [];
|
|
// $monthFilterArray['tsmin'] = $minMonthTimestamp;
|
|
// $monthFilterArray['tsmax'] = $maxMonthTimestamp;
|
|
|
|
$qb = $this->db->getQueryBuilder();
|
|
|
|
foreach ($this->userManager->search('') as $u) {
|
|
$uid = $u->getUID();
|
|
$outPath = $this->config->getUserValue($uid, 'cospend', 'outputDirectory', '/Cospend');
|
|
|
|
$qb->select('id', 'name', 'autoexport')
|
|
->from('cospend_projects')
|
|
->where(
|
|
$qb->expr()->eq('userid', $qb->createNamedParameter($uid, IQueryBuilder::PARAM_STR))
|
|
)
|
|
->andWhere(
|
|
$qb->expr()->neq('autoexport', $qb->createNamedParameter(Application::FREQUENCY_NO, IQueryBuilder::PARAM_STR))
|
|
);
|
|
$req = $qb->executeQuery();
|
|
|
|
$dbProjectId = null;
|
|
while ($row = $req->fetch()) {
|
|
$dbProjectId = $row['id'];
|
|
$autoexport = $row['autoexport'];
|
|
|
|
$suffix = $dailySuffix;
|
|
// TODO add suffix for all frequencies
|
|
if ($autoexport === Application::FREQUENCY_WEEKLY) {
|
|
$suffix = $weeklySuffix;
|
|
} elseif ($autoexport === Application::FREQUENCY_MONTHLY) {
|
|
$suffix = $monthlySuffix;
|
|
}
|
|
// check if file already exists
|
|
$exportName = $dbProjectId . $suffix . '.csv';
|
|
|
|
$userFolder = $this->root->getUserFolder($uid);
|
|
if (!$userFolder->nodeExists($outPath . '/' . $exportName)) {
|
|
$projectInfo = $this->localProjectService->getProjectInfoWithAccessLevel($dbProjectId, $uid);
|
|
$bills = $this->localProjectService->getBills($dbProjectId);
|
|
$this->exportCsvProject($dbProjectId, $uid, $projectInfo, $bills, $exportName);
|
|
}
|
|
}
|
|
$req->closeCursor();
|
|
$qb = $this->db->getQueryBuilder();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create directory where things will be exported
|
|
*
|
|
* @param Folder $userFolder
|
|
* @param string $outPath
|
|
* @return string
|
|
* @throws NotFoundException
|
|
* @throws NotPermittedException
|
|
*/
|
|
private function createAndCheckExportDirectory(Folder $userFolder, string $outPath): string {
|
|
if (!$userFolder->nodeExists($outPath)) {
|
|
$userFolder->newFolder($outPath);
|
|
}
|
|
if ($userFolder->nodeExists($outPath)) {
|
|
$folder = $userFolder->get($outPath);
|
|
if (!$folder instanceof Folder) {
|
|
return $this->l10n->t('%1$s is not a folder', [$outPath]);
|
|
} elseif (!$folder->isCreatable()) {
|
|
return $this->l10n->t('%1$s is not writeable', [$outPath]);
|
|
} else {
|
|
return '';
|
|
}
|
|
} else {
|
|
return $this->l10n->t('Impossible to create %1$s', [$outPath]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export settlement plan in CSV
|
|
* controller get the settlement with IProjectService->getSettlement and then calls CospendService export(settlement) method
|
|
* to store it in the current user's storage
|
|
*
|
|
* @param string $projectId
|
|
* @param string $userId
|
|
* @param array $settlement
|
|
* @param array $members
|
|
* @return array
|
|
* @throws NoUserException
|
|
* @throws NotFoundException
|
|
* @throws NotPermittedException
|
|
* @throws InvalidPathException
|
|
* @throws LockedException
|
|
*/
|
|
public function exportCsvSettlement(string $projectId, string $userId, array $settlement, array $members): array {
|
|
// create export directory if needed
|
|
$outPath = $this->config->getUserValue($userId, 'cospend', 'outputDirectory', '/Cospend');
|
|
$userFolder = $this->root->getUserFolder($userId);
|
|
$msg = $this->createAndCheckExportDirectory($userFolder, $outPath);
|
|
if ($msg !== '') {
|
|
return ['message' => $msg];
|
|
}
|
|
$folder = $userFolder->get($outPath);
|
|
if (!$folder instanceof Folder) {
|
|
return ['message' => $outPath . ' is not a directory'];
|
|
}
|
|
|
|
// create file
|
|
if ($folder->nodeExists($projectId.'-settlement.csv')) {
|
|
$folder->get($projectId.'-settlement.csv')->delete();
|
|
}
|
|
$file = $folder->newFile($projectId.'-settlement.csv');
|
|
$handler = $file->fopen('w');
|
|
fwrite(
|
|
$handler,
|
|
'"' . $this->l10n->t('Who pays?')
|
|
. '","' . $this->l10n->t('To whom?')
|
|
. '","' . $this->l10n->t('How much?')
|
|
. '"' . "\n"
|
|
);
|
|
$transactions = $settlement['transactions'];
|
|
|
|
$memberIdToName = [];
|
|
foreach ($members as $member) {
|
|
$memberIdToName[$member['id']] = $member['name'];
|
|
}
|
|
|
|
foreach ($transactions as $transaction) {
|
|
fwrite(
|
|
$handler,
|
|
'"' . $memberIdToName[$transaction['from']]
|
|
. '","' . $memberIdToName[$transaction['to']]
|
|
. '",' . (float) $transaction['amount']
|
|
. "\n"
|
|
);
|
|
}
|
|
|
|
fclose($handler);
|
|
$file->touch();
|
|
return ['path' => $outPath . '/' . $projectId . '-settlement.csv'];
|
|
}
|
|
|
|
/**
|
|
* controller get the stats with IProjectService->getStatistics and then calls CospendService export(stats) method
|
|
* to store it in the current user's storage
|
|
*
|
|
* @param string $projectId
|
|
* @param string $userId
|
|
* @param array $statistics
|
|
* @return array
|
|
* @throws InvalidPathException
|
|
* @throws LockedException
|
|
* @throws NoUserException
|
|
* @throws NotFoundException
|
|
* @throws NotPermittedException
|
|
*/
|
|
public function exportCsvStatistics(
|
|
string $projectId, string $userId, array $statistics
|
|
): array {
|
|
// create export directory if needed
|
|
$outPath = $this->config->getUserValue($userId, 'cospend', 'outputDirectory', '/Cospend');
|
|
$userFolder = $this->root->getUserFolder($userId);
|
|
$msg = $this->createAndCheckExportDirectory($userFolder, $outPath);
|
|
if ($msg !== '') {
|
|
return ['message' => $msg];
|
|
}
|
|
$folder = $userFolder->get($outPath);
|
|
if (!$folder instanceof Folder) {
|
|
return ['message' => $outPath . ' is not a directory'];
|
|
}
|
|
|
|
// create file
|
|
if ($folder->nodeExists($projectId.'-stats.csv')) {
|
|
$folder->get($projectId.'-stats.csv')->delete();
|
|
}
|
|
$file = $folder->newFile($projectId.'-stats.csv');
|
|
$handler = $file->fopen('w');
|
|
fwrite(
|
|
$handler,
|
|
$this->l10n->t('Member name')
|
|
. ',' . $this->l10n->t('Paid')
|
|
. ',' . $this->l10n->t('Spent')
|
|
. ',' . $this->l10n->t('Balance')
|
|
. "\n"
|
|
);
|
|
$stats = $statistics['stats'];
|
|
|
|
foreach ($stats as $stat) {
|
|
fwrite(
|
|
$handler,
|
|
'"' . $stat['member']['name']
|
|
. '",' . (float) $stat['paid']
|
|
. ',' . (float) $stat['spent']
|
|
. ',' . (float) $stat['balance']
|
|
. "\n"
|
|
);
|
|
}
|
|
|
|
fclose($handler);
|
|
$file->touch();
|
|
return ['path' => $outPath . '/' . $projectId . '-stats.csv'];
|
|
}
|
|
|
|
/**
|
|
* Export project in CSV
|
|
*
|
|
* @param string $projectId
|
|
* @param string $userId
|
|
* @param array $projectInfo
|
|
* @param array $bills
|
|
* @param string|null $name
|
|
* @return array
|
|
* @throws InvalidPathException
|
|
* @throws LockedException
|
|
* @throws NoUserException
|
|
* @throws NotFoundException
|
|
* @throws NotPermittedException
|
|
*/
|
|
public function exportCsvProject(string $projectId, string $userId, array $projectInfo, array $bills, ?string $name = null): array {
|
|
// create export directory if needed
|
|
$outPath = $this->config->getUserValue($userId, 'cospend', 'outputDirectory', '/Cospend');
|
|
$userFolder = $this->root->getUserFolder($userId);
|
|
$msg = $this->createAndCheckExportDirectory($userFolder, $outPath);
|
|
if ($msg !== '') {
|
|
return ['message' => $msg];
|
|
}
|
|
$folder = $userFolder->get($outPath);
|
|
if (!$folder instanceof Folder) {
|
|
return ['message' => $outPath . ' is not a directory'];
|
|
}
|
|
|
|
// create file
|
|
$filename = $projectId.'.csv';
|
|
if ($name !== null) {
|
|
$filename = $name;
|
|
if (!str_ends_with($filename, '.csv')) {
|
|
$filename .= '.csv';
|
|
}
|
|
}
|
|
if ($folder->nodeExists($filename)) {
|
|
$folder->get($filename)->delete();
|
|
}
|
|
$file = $folder->newFile($filename);
|
|
$handler = $file->fopen('w');
|
|
foreach ($this->getJsonProject($projectInfo, $bills) as $chunk) {
|
|
fwrite($handler, $chunk);
|
|
}
|
|
|
|
fclose($handler);
|
|
$file->touch();
|
|
return ['path' => $outPath . '/' . $filename];
|
|
}
|
|
|
|
/**
|
|
* @param array $projectInfo
|
|
* @param array $bills
|
|
* @return Generator
|
|
*/
|
|
public function getJsonProject(array $projectInfo, array $bills): Generator {
|
|
// members
|
|
yield "name,weight,active,color\n";
|
|
$members = $projectInfo['members'];
|
|
$memberIdToName = [];
|
|
$memberIdToWeight = [];
|
|
$memberIdToActive = [];
|
|
foreach ($members as $member) {
|
|
$memberIdToName[$member['id']] = $member['name'];
|
|
$memberIdToWeight[$member['id']] = $member['weight'];
|
|
$memberIdToActive[$member['id']] = (int) $member['activated'];
|
|
$c = $member['color'];
|
|
yield '"' . $member['name'] . '",'
|
|
. (float) $member['weight'] . ','
|
|
. (int) $member['activated'] . ',"'
|
|
. sprintf("#%02x%02x%02x", $c['r'] ?? 0, $c['g'] ?? 0, $c['b'] ?? 0) . '"'
|
|
. "\n";
|
|
}
|
|
// bills
|
|
yield "\nwhat,amount,date,timestamp,payer_name,payer_weight,payer_active,owers,repeat,repeatfreq,repeatallactive,repeatuntil,categoryid,paymentmode,paymentmodeid,comment,deleted\n";
|
|
foreach ($bills as $bill) {
|
|
$owerNames = [];
|
|
foreach ($bill['owers'] as $ower) {
|
|
$owerNames[] = $ower['name'];
|
|
}
|
|
$owersTxt = implode(',', $owerNames);
|
|
|
|
$payer_id = $bill['payer_id'];
|
|
$payer_name = $memberIdToName[$payer_id];
|
|
$payer_weight = $memberIdToWeight[$payer_id];
|
|
$payer_active = $memberIdToActive[$payer_id];
|
|
$dateTime = DateTime::createFromFormat('U', $bill['timestamp']);
|
|
$oldDateStr = $dateTime->format('Y-m-d');
|
|
yield '"' . $bill['what'] . '",'
|
|
. (float) $bill['amount'] . ','
|
|
. $oldDateStr . ','
|
|
. $bill['timestamp'] . ',"'
|
|
. $payer_name . '",'
|
|
. (float) $payer_weight . ','
|
|
. $payer_active . ',"'
|
|
. $owersTxt . '",'
|
|
. $bill['repeat'] . ','
|
|
. $bill['repeatfreq'] . ','
|
|
. $bill['repeatallactive'] .','
|
|
. $bill['repeatuntil'] . ','
|
|
. $bill['categoryid'] . ','
|
|
. $bill['paymentmode'] . ','
|
|
. $bill['paymentmodeid'] . ',"'
|
|
. urlencode($bill['comment']) . '",'
|
|
. $bill['deleted']
|
|
. "\n";
|
|
}
|
|
|
|
// write categories
|
|
$categories = $projectInfo['categories'];
|
|
if (count($categories) > 0) {
|
|
yield "\ncategoryname,categoryid,icon,color\n";
|
|
foreach ($categories as $id => $cat) {
|
|
yield '"' . $cat['name'] . '",' .
|
|
(int) $id . ',"' .
|
|
$cat['icon'] . '","' .
|
|
$cat['color'] . '"' .
|
|
"\n";
|
|
}
|
|
}
|
|
|
|
// write payment modes
|
|
$paymentModes = $projectInfo['paymentmodes'];
|
|
if (count($paymentModes) > 0) {
|
|
yield "\npaymentmodename,paymentmodeid,icon,color\n";
|
|
foreach ($paymentModes as $id => $pm) {
|
|
yield '"' . $pm['name'] . '",' .
|
|
(int) $id . ',"' .
|
|
$pm['icon'] . '","' .
|
|
$pm['color'] . '"' .
|
|
"\n";
|
|
}
|
|
}
|
|
|
|
// write currencies
|
|
$currencies = $projectInfo['currencies'];
|
|
if (count($currencies) > 0) {
|
|
yield "\ncurrencyname,exchange_rate\n";
|
|
// main currency
|
|
yield '"' . $projectInfo['currencyname'] . '",1' . "\n";
|
|
foreach ($currencies as $cur) {
|
|
yield '"' . $cur['name']
|
|
. '",' . (float) $cur['exchange_rate']
|
|
. "\n";
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
}
|