Add tests for Coolify app payload handling, e-conomic customer fields, and expand Coolify API client capabilities
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -64,6 +64,11 @@ class coolify_api_client
|
||||
return $this->request('GET', '/services');
|
||||
}
|
||||
|
||||
public function listGithubApps(): array
|
||||
{
|
||||
return $this->request('GET', '/github-apps');
|
||||
}
|
||||
|
||||
public function getService(string $uuid): array
|
||||
{
|
||||
return $this->request('GET', '/services/' . rawurlencode($uuid));
|
||||
@@ -110,6 +115,12 @@ class coolify_api_client
|
||||
return $this->request('PATCH', '/services/' . rawurlencode($uuid) . '/envs/bulk', ['data' => $data]);
|
||||
}
|
||||
|
||||
public function deployResource(string $uuid, bool $force = false): array
|
||||
{
|
||||
$path = '/deploy?uuid=' . rawurlencode($uuid) . '&force=' . ($force ? 'true' : 'false');
|
||||
return $this->request('GET', $path);
|
||||
}
|
||||
|
||||
public function startService(string $uuid): array
|
||||
{
|
||||
return $this->request('GET', '/services/' . rawurlencode($uuid) . '/start');
|
||||
|
||||
@@ -165,9 +165,17 @@ class economic implements economic_i
|
||||
/**
|
||||
* Create a customer in e-conomic and return the raw upstream payload.
|
||||
*/
|
||||
public function createCustomer(int $customer_number, string $name, int $cvr_number, string $email, int $phone): object
|
||||
public function createCustomer(
|
||||
int $customer_number,
|
||||
string $name,
|
||||
int $cvr_number,
|
||||
string $email,
|
||||
int $phone,
|
||||
?int $mobile_phone = null,
|
||||
object|array|null $company_information = null
|
||||
): object
|
||||
{
|
||||
return $this->customers->customers->create([
|
||||
$payload = [
|
||||
'customerNumber' => $customer_number,
|
||||
'corporateIdentificationNumber' => (string)$cvr_number,
|
||||
'customerGroup' => [
|
||||
@@ -179,11 +187,60 @@ class economic implements economic_i
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'phone' => $phone,
|
||||
'telephoneAndFaxNumber' => (string)$phone,
|
||||
'mobilePhone' => (string)($mobile_phone ?? $phone),
|
||||
'currency' => 'DKK',
|
||||
'vatZone' => [
|
||||
'vatZoneNumber' => 1,
|
||||
]
|
||||
]);
|
||||
];
|
||||
|
||||
$payload = array_replace($payload, $this->buildCustomerPayloadFromCompanyInformation($company_information));
|
||||
|
||||
return $this->customers->customers->create($payload);
|
||||
}
|
||||
|
||||
private function buildCustomerPayloadFromCompanyInformation(object|array|null $company_information): array
|
||||
{
|
||||
if ($company_information === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$payload = [];
|
||||
$field_map = [
|
||||
'address' => 'address',
|
||||
'zipcode' => 'zip',
|
||||
'city' => 'city',
|
||||
'website' => 'website',
|
||||
];
|
||||
|
||||
foreach ($field_map as $source_field => $economic_field) {
|
||||
$value = $this->companyInformationValue($company_information, $source_field);
|
||||
if ($value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$payload[$economic_field] = $value;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function companyInformationValue(object|array $company_information, string $field): ?string
|
||||
{
|
||||
if (is_array($company_information)) {
|
||||
$value = $company_information[$field] ?? null;
|
||||
} else {
|
||||
$value = $company_information->{$field} ?? null;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = trim((string)$value);
|
||||
|
||||
return $normalized !== '' ? $normalized : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,7 @@ class release_manager
|
||||
private const SERVICE_SET_MODES = ['attach_existing', 'clone_existing', 'fresh_empty', 'isolated_stack'];
|
||||
private const STACK_DATA_KINDS = ['database', 'redis', 'minio'];
|
||||
private const DEFAULT_COOLIFY_ENVIRONMENT_CHANNELS = ['stable', 'production', 'prod', 'beta'];
|
||||
private const DEFAULT_COOLIFY_APPLICATION_PORT = '80';
|
||||
private const SUBJECT_TYPES = ['user', 'subuser', 'customer'];
|
||||
private const CAPTURE_LEVELS = ['metadata', 'full_redacted', 'full'];
|
||||
private const MODULE_KEYS = [
|
||||
@@ -1066,6 +1067,73 @@ class release_manager
|
||||
return $this->publicServiceSet($this->getServiceSet($id));
|
||||
}
|
||||
|
||||
public function deleteServiceSet(int $id, array $input = [], ?int $actorUserId = null): array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
$serviceSet = $this->getServiceSet($id);
|
||||
if ((string)($serviceSet['mode'] ?? '') !== 'isolated_stack') {
|
||||
throw new RuntimeException('Only isolated stack service sets can be removed from Release Manager.');
|
||||
}
|
||||
if ($this->serviceSetIsActive($id)) {
|
||||
throw new RuntimeException('The active release service set cannot be removed.');
|
||||
}
|
||||
|
||||
$frontendTargetId = $this->nullablePositiveInt($serviceSet['frontend_target_id'] ?? null);
|
||||
$apiTargetId = $this->nullablePositiveInt($serviceSet['api_target_id'] ?? null);
|
||||
$dataTargetIds = [];
|
||||
foreach (self::STACK_DATA_KINDS as $kind) {
|
||||
$dataTargetIds[$kind] = $this->nullablePositiveInt($serviceSet[$kind . '_coolify_target_id'] ?? null);
|
||||
}
|
||||
|
||||
$this->execute(
|
||||
"UPDATE release_bundles
|
||||
SET status = 'removed', deleted_at = NOW()
|
||||
WHERE service_set_id = ? AND deleted_at IS NULL",
|
||||
'i',
|
||||
[$id]
|
||||
);
|
||||
$this->execute(
|
||||
"UPDATE release_deployments
|
||||
SET status = 'removed'
|
||||
WHERE service_set_id = ?",
|
||||
'i',
|
||||
[$id]
|
||||
);
|
||||
$this->execute(
|
||||
"UPDATE release_service_sets
|
||||
SET status = 'removed', deleted_at = NOW(), actor_user_id = ?
|
||||
WHERE id = ?",
|
||||
'ii',
|
||||
[$actorUserId, $id]
|
||||
);
|
||||
|
||||
foreach ([$frontendTargetId, $apiTargetId] as $targetId) {
|
||||
if ($this->isolatedDeploymentTargetCanBeForgotten($targetId, $id)) {
|
||||
$this->execute('UPDATE release_deployment_targets SET deleted_at = NOW() WHERE id = ?', 'i', [$targetId]);
|
||||
}
|
||||
}
|
||||
foreach ($dataTargetIds as $targetId) {
|
||||
if ($this->isolatedCoolifyTargetCanBeForgotten($targetId, $id)) {
|
||||
$this->execute('UPDATE coolify_targets SET deleted_at = NOW() WHERE id = ?', 'i', [$targetId]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->audit((int)$serviceSet['channel_id'], null, 'service_set_removed', $actorUserId, 'warning', [
|
||||
'service_set_id' => $id,
|
||||
'mode' => 'isolated_stack',
|
||||
'provider_resources_deleted' => false,
|
||||
'frontend_target_id' => $frontendTargetId,
|
||||
'api_target_id' => $apiTargetId,
|
||||
'data_target_ids' => $dataTargetIds,
|
||||
]);
|
||||
|
||||
return [
|
||||
'id' => $id,
|
||||
'removed' => true,
|
||||
'provider_resources_deleted' => false,
|
||||
];
|
||||
}
|
||||
|
||||
public function completeIsolatedStackDataServices(int $serviceSetId, array $input = [], ?int $actorUserId = null): array
|
||||
{
|
||||
$this->ensureSchema();
|
||||
@@ -1308,6 +1376,20 @@ class release_manager
|
||||
$serviceSetId = (int)$bundle['service_set_id'];
|
||||
|
||||
$this->execute('UPDATE release_channel_versions SET active = 0 WHERE channel_id = ?', 'i', [$channelId]);
|
||||
$this->execute(
|
||||
"UPDATE release_bundles
|
||||
SET status = 'superseded'
|
||||
WHERE channel_id = ? AND id <> ? AND status = 'promoted' AND deleted_at IS NULL",
|
||||
'ii',
|
||||
[$channelId, $bundleId]
|
||||
);
|
||||
$this->execute(
|
||||
"UPDATE release_deployments
|
||||
SET status = 'superseded'
|
||||
WHERE channel_id = ? AND bundle_id IS NOT NULL AND bundle_id <> ? AND status = 'active'",
|
||||
'ii',
|
||||
[$channelId, $bundleId]
|
||||
);
|
||||
$this->execute(
|
||||
"INSERT INTO release_channel_versions (
|
||||
channel_id, frontend_version_id, api_version_id, deployment_id,
|
||||
@@ -1478,7 +1560,12 @@ class release_manager
|
||||
$result = ['message' => 'Deployment recorded; no Coolify service target is configured.'];
|
||||
$status = 'queued';
|
||||
if ($target !== null && !empty($target['coolify_instance_id'])) {
|
||||
$result = $this->deployCoolifyReleaseTarget($target);
|
||||
$coolifyTarget = array_replace($target, [
|
||||
'repository' => $repository,
|
||||
'branch' => $branch,
|
||||
'commit_sha' => $commitSha ?? '',
|
||||
]);
|
||||
$result = $this->deployCoolifyReleaseTarget($coolifyTarget);
|
||||
$status = 'deployed';
|
||||
}
|
||||
|
||||
@@ -2304,10 +2391,15 @@ class release_manager
|
||||
$created = null;
|
||||
|
||||
if ($serviceUuid === '' && $this->toBool($context['coolify_auto_create'] ?? false)) {
|
||||
if ($this->releaseCoolifyGithubAppUuid($context) !== '') {
|
||||
$githubAppUuid = $this->releaseCoolifyGithubAppUuid($context, $target, $instance, true);
|
||||
if ($githubAppUuid !== '') {
|
||||
$context['coolify_github_app_uuid'] = $githubAppUuid;
|
||||
$created = $client->createPrivateGithubAppApplication($this->releaseCoolifyApplicationPayload($target, $context, $instance));
|
||||
$resourceType = 'application';
|
||||
} else {
|
||||
if ($this->releaseCoolifyServiceSourceIsMissing($context)) {
|
||||
throw new RuntimeException('Release Manager could not resolve a Coolify GitHub App for this private source repository. Configure one GitHub App in Coolify or set coolify_github_app_uuid on the target; no source-code credentials are required in Release Manager.');
|
||||
}
|
||||
$created = $client->createService($this->releaseCoolifyServicePayload($target, $context, $instance));
|
||||
$resourceType = 'service';
|
||||
}
|
||||
@@ -2329,28 +2421,29 @@ class release_manager
|
||||
|
||||
$update = null;
|
||||
$publicUrl = $this->releaseCoolifyPublicUrl($target, $context);
|
||||
if ($this->toBool($context['coolify_enable_ssl'] ?? false) && $publicUrl !== null) {
|
||||
if ($resourceType === 'application') {
|
||||
$update = $client->updateApplication($serviceUuid, [
|
||||
'domains' => $publicUrl,
|
||||
'force_domain_override' => true,
|
||||
]);
|
||||
} else {
|
||||
$update = $client->updateService($serviceUuid, [
|
||||
'urls' => [
|
||||
[
|
||||
'name' => (string)($target['app'] ?? 'release'),
|
||||
'url' => $publicUrl,
|
||||
],
|
||||
],
|
||||
'force_domain_override' => true,
|
||||
]);
|
||||
if ($resourceType === 'application') {
|
||||
$applicationUpdate = $this->releaseCoolifyApplicationUpdatePayload($target, $context);
|
||||
if ($this->toBool($context['coolify_enable_ssl'] ?? false) && $publicUrl !== null) {
|
||||
$applicationUpdate['domains'] = $publicUrl;
|
||||
$applicationUpdate['is_force_https_enabled'] = true;
|
||||
$applicationUpdate['force_domain_override'] = true;
|
||||
}
|
||||
if ($applicationUpdate !== []) {
|
||||
$update = $client->updateApplication($serviceUuid, $applicationUpdate);
|
||||
}
|
||||
} elseif ($this->toBool($context['coolify_enable_ssl'] ?? false) && $publicUrl !== null) {
|
||||
$update = $client->updateService($serviceUuid, [
|
||||
'urls' => [
|
||||
[
|
||||
'name' => (string)($target['app'] ?? 'release'),
|
||||
'url' => $publicUrl,
|
||||
],
|
||||
],
|
||||
'force_domain_override' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$restart = $resourceType === 'application'
|
||||
? $client->restartApplication($serviceUuid)
|
||||
: $client->restartService($serviceUuid);
|
||||
$deployment = $client->deployResource($serviceUuid, $this->releaseCoolifyForceRebuild($context));
|
||||
return [
|
||||
'service_uuid' => $serviceUuid,
|
||||
'resource_type' => $resourceType,
|
||||
@@ -2358,7 +2451,7 @@ class release_manager
|
||||
'public_url' => $publicUrl,
|
||||
'created' => self::redactPayload($created ?? []),
|
||||
'updated' => self::redactPayload($update ?? []),
|
||||
'restart' => self::redactPayload($restart),
|
||||
'deployment' => self::redactPayload($deployment),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2370,9 +2463,9 @@ class release_manager
|
||||
throw new RuntimeException('Select a Coolify project for this release target before creating an application.');
|
||||
}
|
||||
|
||||
$githubAppUuid = $this->releaseCoolifyGithubAppUuid($context);
|
||||
$githubAppUuid = $this->releaseCoolifyGithubAppUuid($context, $target, $instance, true);
|
||||
if ($githubAppUuid === '') {
|
||||
throw new RuntimeException('Coolify GitHub App UUID is required so Coolify can pull with the app token.');
|
||||
throw new RuntimeException('Release Manager could not resolve a Coolify GitHub App UUID so Coolify can pull with the app token.');
|
||||
}
|
||||
|
||||
$serverUuid = $this->releaseCoolifyServerUuid($context, $instance);
|
||||
@@ -2397,12 +2490,18 @@ class release_manager
|
||||
'github_app_uuid' => $githubAppUuid,
|
||||
'git_repository' => $repository,
|
||||
'git_branch' => trim((string)($target['branch'] ?? self::DEFAULT_BRANCH)) ?: self::DEFAULT_BRANCH,
|
||||
'git_commit_sha' => $this->releaseCoolifyGitCommitSha($target, $context),
|
||||
'build_pack' => $this->releaseCoolifyBuildPack($target, $context),
|
||||
'ports_exposes' => $this->releaseCoolifyPortsExposes($target, $context),
|
||||
'instant_deploy' => $this->toBool($context['coolify_deploy_now'] ?? true),
|
||||
'is_auto_deploy_enabled' => $this->toBool($target['auto_deploy'] ?? true),
|
||||
'force_domain_override' => true,
|
||||
];
|
||||
|
||||
foreach ($this->releaseCoolifyApplicationDefaultFields($target, $context) as $key => $value) {
|
||||
$payload[$key] = $value;
|
||||
}
|
||||
|
||||
if ($publicUrl !== null) {
|
||||
$payload['domains'] = $publicUrl;
|
||||
$payload['is_force_https_enabled'] = $this->toBool($context['coolify_enable_ssl'] ?? false);
|
||||
@@ -2415,6 +2514,27 @@ class release_manager
|
||||
return array_filter($payload, static fn(mixed $value): bool => $value !== null && $value !== '');
|
||||
}
|
||||
|
||||
private function releaseCoolifyApplicationUpdatePayload(array $target, array $context): array
|
||||
{
|
||||
$payload = [
|
||||
'git_repository' => trim((string)($target['repository'] ?? '')),
|
||||
'git_branch' => trim((string)($target['branch'] ?? self::DEFAULT_BRANCH)) ?: self::DEFAULT_BRANCH,
|
||||
'git_commit_sha' => $this->releaseCoolifyGitCommitSha($target, $context),
|
||||
'build_pack' => $this->releaseCoolifyBuildPack($target, $context),
|
||||
'ports_exposes' => $this->releaseCoolifyPortsExposes($target, $context),
|
||||
];
|
||||
|
||||
foreach ($this->releaseCoolifyApplicationDefaultFields($target, $context) as $key => $value) {
|
||||
$payload[$key] = $value;
|
||||
}
|
||||
|
||||
foreach ($this->releaseCoolifyApplicationOptionalFields($context) as $key => $value) {
|
||||
$payload[$key] = $value;
|
||||
}
|
||||
|
||||
return array_filter($payload, static fn(mixed $value): bool => $value !== null && $value !== '');
|
||||
}
|
||||
|
||||
private function releaseCoolifyServicePayload(array $target, array $context, array $instance): array
|
||||
{
|
||||
$publicUrl = $this->releaseCoolifyPublicUrl($target, $context);
|
||||
@@ -2480,16 +2600,101 @@ class release_manager
|
||||
);
|
||||
}
|
||||
|
||||
private function releaseCoolifyGithubAppUuid(array $context): string
|
||||
private function releaseCoolifyServiceSourceIsMissing(array $context): bool
|
||||
{
|
||||
foreach (['coolify_github_app_uuid', 'github_app_uuid', 'coolify_git_app_uuid', 'git_app_uuid'] as $key) {
|
||||
return trim((string)($context['docker_compose_raw'] ?? '')) === ''
|
||||
&& $this->releaseCoolifyExplicitImage($context) === ''
|
||||
&& !$this->toBool($context['coolify_assume_ghcr_image'] ?? $context['assume_ghcr_image'] ?? false);
|
||||
}
|
||||
|
||||
private function releaseCoolifyGithubAppUuid(array $context, array $target = [], array $instance = [], bool $discover = false): string
|
||||
{
|
||||
foreach ([
|
||||
'coolify_github_app_uuid',
|
||||
'github_app_uuid',
|
||||
'coolify_git_app_uuid',
|
||||
'git_app_uuid',
|
||||
'default_github_app_uuid',
|
||||
'default_coolify_github_app_uuid',
|
||||
] as $key) {
|
||||
$value = trim((string)($context[$key] ?? ''));
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
if (!$discover) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->releaseCoolifyDefaultGithubAppUuid($target, $context, $instance);
|
||||
}
|
||||
|
||||
private function releaseCoolifyDefaultGithubAppUuid(array $target, array $context, array $instance): string
|
||||
{
|
||||
foreach ([
|
||||
'default_github_app_uuid',
|
||||
'default_coolify_github_app_uuid',
|
||||
'coolify_github_app_uuid',
|
||||
'github_app_uuid',
|
||||
] as $key) {
|
||||
$value = trim((string)($instance[$key] ?? ''));
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ([
|
||||
getenv('RELEASE_MANAGER_COOLIFY_GITHUB_APP_UUID') ?: ($_SERVER['RELEASE_MANAGER_COOLIFY_GITHUB_APP_UUID'] ?? null),
|
||||
getenv('COOLIFY_GITHUB_APP_UUID') ?: ($_SERVER['COOLIFY_GITHUB_APP_UUID'] ?? null),
|
||||
$this->moduleConfigValue('ReleaseManager', 'coolify_github_app_uuid', ''),
|
||||
$this->moduleConfigValue('Coolify', 'github_app_uuid', ''),
|
||||
] as $value) {
|
||||
$value = trim((string)$value);
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
$tokenSecret = trim((string)($instance['api_token_secret'] ?? ''));
|
||||
if ($tokenSecret === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
$token = replication_secret_box::decrypt($tokenSecret);
|
||||
$apps = (new coolify_api_client((string)($instance['base_url'] ?? ''), $token, 4))->listGithubApps();
|
||||
} catch (Throwable) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$rows = array_values(array_filter($this->payloadRows($apps), static function (mixed $row): bool {
|
||||
return is_array($row) && trim((string)($row['uuid'] ?? '')) !== '';
|
||||
}));
|
||||
if (count($rows) === 1) {
|
||||
return trim((string)$rows[0]['uuid']);
|
||||
}
|
||||
|
||||
$repository = self::normalizeGithubRepositoryName((string)($target['repository'] ?? ''));
|
||||
$owner = strtolower(trim(strtok($repository, '/') ?: ''));
|
||||
if ($owner === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$matches = array_values(array_filter($rows, static function (array $row) use ($owner): bool {
|
||||
foreach (['organization', 'name', 'custom_user', 'html_url'] as $key) {
|
||||
$value = strtolower(trim((string)($row[$key] ?? '')));
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
if ($value === $owner || str_contains($value, '/' . $owner) || str_contains($value, $owner . '-')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
|
||||
return count($matches) === 1 ? trim((string)$matches[0]['uuid']) : '';
|
||||
}
|
||||
|
||||
private function releaseCoolifyResourceType(array $context, string $serviceUuid = ''): string
|
||||
@@ -2507,12 +2712,90 @@ class release_manager
|
||||
|
||||
private function releaseCoolifyBuildPack(array $target, array $context): string
|
||||
{
|
||||
$app = strtolower(trim((string)($target['app'] ?? '')));
|
||||
$buildPack = strtolower(trim((string)($context['coolify_build_pack'] ?? $context['build_pack'] ?? '')));
|
||||
if ($buildPack !== '') {
|
||||
if ($app === 'frontend' && $buildPack === 'nixpacks') {
|
||||
return 'static';
|
||||
}
|
||||
return $buildPack;
|
||||
}
|
||||
|
||||
return (string)($target['app'] ?? '') === 'api' ? 'dockerfile' : 'nixpacks';
|
||||
return $app === 'api' ? 'dockerfile' : 'static';
|
||||
}
|
||||
|
||||
private function releaseCoolifyPortsExposes(array $target, array $context): string
|
||||
{
|
||||
foreach ([
|
||||
'coolify_ports_exposes',
|
||||
'ports_exposes',
|
||||
'coolify_exposed_port',
|
||||
'exposed_port',
|
||||
'coolify_port',
|
||||
'port',
|
||||
] as $key) {
|
||||
$value = trim((string)($context[$key] ?? ''));
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
$app = strtolower(trim((string)($target['app'] ?? '')));
|
||||
$envKeys = $app === 'api'
|
||||
? ['RELEASE_MANAGER_API_PORTS_EXPOSES', 'RELEASE_API_PORTS_EXPOSES', 'API_PORTS_EXPOSES']
|
||||
: ['RELEASE_MANAGER_FRONTEND_PORTS_EXPOSES', 'RELEASE_FRONTEND_PORTS_EXPOSES', 'FRONTEND_PORTS_EXPOSES'];
|
||||
foreach ($envKeys as $key) {
|
||||
$value = trim((string)(getenv($key) ?: ($_SERVER[$key] ?? '')));
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return self::DEFAULT_COOLIFY_APPLICATION_PORT;
|
||||
}
|
||||
|
||||
private function releaseCoolifyGitCommitSha(array $target, array $context): string
|
||||
{
|
||||
foreach ([
|
||||
'coolify_git_commit_sha',
|
||||
'git_commit_sha',
|
||||
'commit_sha',
|
||||
'commit',
|
||||
] as $key) {
|
||||
$value = trim((string)($context[$key] ?? $target[$key] ?? ''));
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function releaseCoolifyForceRebuild(array $context): bool
|
||||
{
|
||||
if (array_key_exists('coolify_force_rebuild', $context) || array_key_exists('force_rebuild', $context)) {
|
||||
return $this->toBool($context['coolify_force_rebuild'] ?? $context['force_rebuild'] ?? false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function releaseCoolifyApplicationDefaultFields(array $target, array $context): array
|
||||
{
|
||||
if (strtolower(trim((string)($target['app'] ?? ''))) !== 'frontend') {
|
||||
return [];
|
||||
}
|
||||
if ($this->releaseCoolifyBuildPack($target, $context) !== 'static') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'install_command' => 'npm ci',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => 'dist',
|
||||
'is_static' => true,
|
||||
'is_spa' => true,
|
||||
];
|
||||
}
|
||||
|
||||
private function releaseCoolifyApplicationOptionalFields(array $context): array
|
||||
@@ -3244,6 +3527,7 @@ class release_manager
|
||||
$coolifyInstances = [];
|
||||
$coolifyProjects = [];
|
||||
$coolifyServices = [];
|
||||
$coolifyGithubApps = [];
|
||||
if ($this->tableExists('coolify_instances')) {
|
||||
$instanceRows = $this->selectRows(
|
||||
'SELECT id, label, base_url, api_token_secret, status, default_project_uuid, default_environment_uuid, default_environment_name, default_server_uuid
|
||||
@@ -3268,6 +3552,9 @@ class release_manager
|
||||
foreach ($this->coolifyProjectSuggestions($instanceRow) as $project) {
|
||||
$coolifyProjects[] = $project;
|
||||
}
|
||||
foreach ($this->coolifyGithubAppSuggestions($instanceRow) as $githubApp) {
|
||||
$coolifyGithubApps[] = $githubApp;
|
||||
}
|
||||
foreach ($this->coolifyServiceSuggestions($instanceRow) as $service) {
|
||||
$coolifyServices[] = $service;
|
||||
$this->appendSuggestion($serviceUuids, $service['uuid'] ?? null);
|
||||
@@ -3335,6 +3622,7 @@ class release_manager
|
||||
'load_balancer_domains' => array_values($loadBalancerDomains),
|
||||
'coolify_instances' => $coolifyInstances,
|
||||
'coolify_projects' => $coolifyProjects,
|
||||
'coolify_github_apps' => $coolifyGithubApps,
|
||||
'coolify_services' => $coolifyServices,
|
||||
'coolify_service_uuids' => array_values($serviceUuids),
|
||||
'channel_presets' => $channelPresets,
|
||||
@@ -3470,6 +3758,44 @@ class release_manager
|
||||
return array_slice($suggestions, 0, 50);
|
||||
}
|
||||
|
||||
private function coolifyGithubAppSuggestions(array $instance): array
|
||||
{
|
||||
$tokenSecret = trim((string)($instance['api_token_secret'] ?? ''));
|
||||
if ($tokenSecret === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$token = replication_secret_box::decrypt($tokenSecret);
|
||||
$apps = (new coolify_api_client((string)($instance['base_url'] ?? ''), $token, 4))->listGithubApps();
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$suggestions = [];
|
||||
foreach ($this->payloadRows($apps) as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$uuid = trim((string)($row['uuid'] ?? ''));
|
||||
if ($uuid === '') {
|
||||
continue;
|
||||
}
|
||||
$suggestions[] = [
|
||||
'instance_id' => (int)($instance['id'] ?? 0),
|
||||
'instance_label' => (string)($instance['label'] ?? ''),
|
||||
'uuid' => $uuid,
|
||||
'name' => (string)($row['name'] ?? $uuid),
|
||||
'organization' => (string)($row['organization'] ?? ''),
|
||||
'type' => (string)($row['type'] ?? ''),
|
||||
'is_system_wide' => (bool)($row['is_system_wide'] ?? false),
|
||||
'html_url' => (string)($row['html_url'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return array_slice($suggestions, 0, 50);
|
||||
}
|
||||
|
||||
private function coolifyServiceSuggestions(array $instance): array
|
||||
{
|
||||
$tokenSecret = trim((string)($instance['api_token_secret'] ?? ''));
|
||||
@@ -3532,7 +3858,7 @@ class release_manager
|
||||
if (array_keys($payload) === range(0, count($payload) - 1)) {
|
||||
return $payload;
|
||||
}
|
||||
foreach (['data', 'services', 'projects', 'servers', 'results'] as $key) {
|
||||
foreach (['data', 'services', 'projects', 'servers', 'github_apps', 'results'] as $key) {
|
||||
if (is_array($payload[$key] ?? null)) {
|
||||
return $this->payloadRows($payload[$key]);
|
||||
}
|
||||
@@ -4126,9 +4452,10 @@ class release_manager
|
||||
}
|
||||
|
||||
$attachedBundles = $includeBundles ? $this->serviceSetBundles((int)($serviceSet['id'] ?? 0)) : [];
|
||||
$serviceSetId = (int)($serviceSet['id'] ?? 0);
|
||||
|
||||
return [
|
||||
'id' => (int)($serviceSet['id'] ?? 0),
|
||||
'id' => $serviceSetId,
|
||||
'channel_id' => isset($serviceSet['channel_id']) ? (int)$serviceSet['channel_id'] : null,
|
||||
'channel_slug' => $serviceSet['channel_slug'] ?? null,
|
||||
'channel_name' => $serviceSet['channel_name'] ?? null,
|
||||
@@ -4137,6 +4464,7 @@ class release_manager
|
||||
'mode' => (string)($serviceSet['mode'] ?? 'attach_existing'),
|
||||
'source_service_set_id' => isset($serviceSet['source_service_set_id']) ? (int)$serviceSet['source_service_set_id'] : null,
|
||||
'status' => (string)($serviceSet['status'] ?? 'unknown'),
|
||||
'active' => $serviceSetId > 0 && $this->serviceSetIsActive($serviceSetId),
|
||||
'targets' => [
|
||||
'frontend' => $this->nullableDeploymentTarget($this->nullablePositiveInt($serviceSet['frontend_target_id'] ?? null)),
|
||||
'api' => $this->nullableDeploymentTarget($this->nullablePositiveInt($serviceSet['api_target_id'] ?? null)),
|
||||
@@ -4160,13 +4488,19 @@ class release_manager
|
||||
|
||||
private function publicBundle(array $bundle, bool $includeServiceSet = true): array
|
||||
{
|
||||
$bundleId = (int)($bundle['id'] ?? 0);
|
||||
$frontendVersionId = $this->nullablePositiveInt($bundle['frontend_version_id'] ?? null);
|
||||
$apiVersionId = $this->nullablePositiveInt($bundle['api_version_id'] ?? null);
|
||||
$frontendDeploymentId = $this->nullablePositiveInt($bundle['frontend_deployment_id'] ?? null);
|
||||
$apiDeploymentId = $this->nullablePositiveInt($bundle['api_deployment_id'] ?? null);
|
||||
$active = $bundleId > 0 && $this->bundleIsActive($bundleId);
|
||||
$status = (string)($bundle['status'] ?? 'draft');
|
||||
if ($status === 'promoted' && !$active) {
|
||||
$status = 'superseded';
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int)($bundle['id'] ?? 0),
|
||||
'id' => $bundleId,
|
||||
'channel_id' => (int)($bundle['channel_id'] ?? 0),
|
||||
'channel_slug' => (string)($bundle['channel_slug'] ?? ''),
|
||||
'channel_name' => (string)($bundle['channel_name'] ?? ''),
|
||||
@@ -4175,7 +4509,8 @@ class release_manager
|
||||
'service_set_slug' => $bundle['service_set_slug'] ?? null,
|
||||
'service_set' => $includeServiceSet ? $this->publicServiceSet($this->getServiceSet((int)$bundle['service_set_id']), false) : null,
|
||||
'version_label' => $bundle['version_label'] ?? null,
|
||||
'status' => (string)($bundle['status'] ?? 'draft'),
|
||||
'status' => $status,
|
||||
'active' => $active,
|
||||
'apps' => [
|
||||
'frontend' => [
|
||||
'version_id' => $frontendVersionId,
|
||||
@@ -4228,6 +4563,87 @@ class release_manager
|
||||
);
|
||||
}
|
||||
|
||||
private function serviceSetIsActive(int $serviceSetId): bool
|
||||
{
|
||||
return $this->selectOne(
|
||||
'SELECT id FROM release_channel_versions WHERE service_set_id = ? AND active = 1 LIMIT 1',
|
||||
'i',
|
||||
[$serviceSetId]
|
||||
) !== null;
|
||||
}
|
||||
|
||||
private function bundleIsActive(int $bundleId): bool
|
||||
{
|
||||
return $this->selectOne(
|
||||
'SELECT id FROM release_channel_versions WHERE bundle_id = ? AND active = 1 LIMIT 1',
|
||||
'i',
|
||||
[$bundleId]
|
||||
) !== null;
|
||||
}
|
||||
|
||||
private function isolatedDeploymentTargetCanBeForgotten(?int $targetId, int $serviceSetId): bool
|
||||
{
|
||||
if ($targetId === null || $targetId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$target = $this->nullableDeploymentTarget($targetId);
|
||||
if ($target === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$context = is_array($target['deploy_context'] ?? null) ? $target['deploy_context'] : [];
|
||||
if (!$this->toBool($context['isolated_stack'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
if ($this->toBool($context['production_data_attached'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->selectOne(
|
||||
"SELECT id
|
||||
FROM release_service_sets
|
||||
WHERE id <> ? AND deleted_at IS NULL AND (frontend_target_id = ? OR api_target_id = ?)
|
||||
LIMIT 1",
|
||||
'iii',
|
||||
[$serviceSetId, $targetId, $targetId]
|
||||
) === null;
|
||||
}
|
||||
|
||||
private function isolatedCoolifyTargetCanBeForgotten(?int $targetId, int $serviceSetId): bool
|
||||
{
|
||||
if ($targetId === null || $targetId <= 0 || !$this->tableExists('coolify_targets')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$target = $this->nullableCoolifyTarget($targetId);
|
||||
if ($target === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$options = is_array($target['options'] ?? null) ? $target['options'] : [];
|
||||
if (!$this->toBool($options['isolated_stack'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
if ($this->toBool($options['production_data_attached'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->selectOne(
|
||||
"SELECT id
|
||||
FROM release_service_sets
|
||||
WHERE id <> ? AND deleted_at IS NULL
|
||||
AND (
|
||||
database_coolify_target_id = ?
|
||||
OR redis_coolify_target_id = ?
|
||||
OR minio_coolify_target_id = ?
|
||||
)
|
||||
LIMIT 1",
|
||||
'iiii',
|
||||
[$serviceSetId, $targetId, $targetId, $targetId]
|
||||
) === null;
|
||||
}
|
||||
|
||||
private function nullableDeploymentTarget(?int $id): ?array
|
||||
{
|
||||
if ($id === null || $id <= 0) {
|
||||
|
||||
@@ -451,14 +451,17 @@ class authRoute
|
||||
$response->error('Company phone number already registered', 400);
|
||||
}
|
||||
|
||||
// Get the customer name
|
||||
$name = (new virkdata())->getCompanyInformation($cvr, '', [])->name;
|
||||
// Get the CVR company information used for the e-conomic customer payload.
|
||||
$companyInformation = (new virkdata())->getCompanyInformation($cvr, '', []);
|
||||
$name = (string)($companyInformation->name ?? '');
|
||||
$result = $economic->createCustomer(
|
||||
(int)$companyPhone,
|
||||
$name,
|
||||
(int)$cvr,
|
||||
(string)$invoiceEmail,
|
||||
(int)$companyPhone,
|
||||
(int)$contactPhone,
|
||||
$companyInformation,
|
||||
);
|
||||
|
||||
if (!isset($result->customerNumber) || !is_numeric($result->customerNumber)) {
|
||||
|
||||
@@ -245,6 +245,24 @@ class releaseManagerRoute
|
||||
'superuser_release_manager_deploy' => 'Create reusable Release Manager service sets',
|
||||
]);
|
||||
|
||||
$this->delete('/superuser/releases/service-sets/{id}', function () {
|
||||
global $response;
|
||||
$this->requirePermission('superuser_release_manager_deploy');
|
||||
try {
|
||||
$response->success(
|
||||
(new release_manager())->deleteServiceSet(
|
||||
$this->routeId(),
|
||||
$this->requestPayload(),
|
||||
$this->actorUserId()
|
||||
)
|
||||
);
|
||||
} catch (Throwable $throwable) {
|
||||
$response->error(['message' => $throwable->getMessage()], 400);
|
||||
}
|
||||
}, [
|
||||
'superuser_release_manager_deploy' => 'Remove inactive isolated Release Manager service sets',
|
||||
]);
|
||||
|
||||
$this->post('/superuser/releases/service-sets/{id}/isolated-data-services', function () {
|
||||
global $response;
|
||||
$this->requirePermission('superuser_release_manager_deploy');
|
||||
|
||||
@@ -60,4 +60,40 @@ it('returns the raw upstream create response and preserves the requested payload
|
||||
expect($probe->inner->lastPayload['customerNumber'])->toBe(42331123);
|
||||
expect($probe->inner->lastPayload['corporateIdentificationNumber'])->toBe('37781258');
|
||||
expect($probe->inner->lastPayload['phone'])->toBe(42331123);
|
||||
expect($probe->inner->lastPayload['telephoneAndFaxNumber'])->toBe('42331123');
|
||||
expect($probe->inner->lastPayload['mobilePhone'])->toBe('42331123');
|
||||
});
|
||||
|
||||
it('adds supported CVR company fields to the e-conomic customer payload', function (): void {
|
||||
$stubResponse = (object)[
|
||||
'customerNumber' => 42331123,
|
||||
'name' => 'Truckwash ApS',
|
||||
];
|
||||
$companyInformation = (object)[
|
||||
'address' => 'Testvej 12',
|
||||
'zipcode' => 2630,
|
||||
'city' => 'Taastrup',
|
||||
'website' => 'https://truckwash.test',
|
||||
'industrycode' => 953190,
|
||||
];
|
||||
|
||||
$probe = new EconomicCreateCustomerProbe($stubResponse);
|
||||
$result = $probe->createCustomer(
|
||||
42331123,
|
||||
'Truckwash ApS',
|
||||
37781258,
|
||||
'invoice@truckwash.test',
|
||||
42331123,
|
||||
55667788,
|
||||
$companyInformation,
|
||||
);
|
||||
|
||||
expect($result)->toBe($stubResponse);
|
||||
expect($probe->inner->lastPayload['address'])->toBe('Testvej 12');
|
||||
expect($probe->inner->lastPayload['zip'])->toBe('2630');
|
||||
expect($probe->inner->lastPayload['city'])->toBe('Taastrup');
|
||||
expect($probe->inner->lastPayload['website'])->toBe('https://truckwash.test');
|
||||
expect($probe->inner->lastPayload['telephoneAndFaxNumber'])->toBe('42331123');
|
||||
expect($probe->inner->lastPayload['mobilePhone'])->toBe('55667788');
|
||||
expect(array_key_exists('industrycode', $probe->inner->lastPayload))->toBeFalse();
|
||||
});
|
||||
|
||||
@@ -301,7 +301,9 @@ it('defines Coolify schema, route permissions, and replication integration hooks
|
||||
|
||||
$client = file_get_contents(app_path('classes/coolify_api_client.php'));
|
||||
expect($client)->toContain("request('GET', '/health', null, false)");
|
||||
expect($client)->toContain('/github-apps');
|
||||
expect($client)->toContain('/applications/private-github-app');
|
||||
expect($client)->toContain("'/deploy?uuid=' . rawurlencode(\$uuid)");
|
||||
expect($client)->toContain('/applications/\' . rawurlencode($uuid) . \'/restart');
|
||||
expect($client)->toContain('CURL_HTTP_VERSION_1_1');
|
||||
expect($client)->toContain('validationErrorSummary');
|
||||
|
||||
@@ -163,11 +163,70 @@ it('creates Coolify GitHub App application payloads so pulls use the app token',
|
||||
expect($payload['github_app_uuid'])->toBe('github-app-copenhagentruckwash-github');
|
||||
expect($payload['git_repository'])->toBe('copenhagentruckwash/pleno-vue');
|
||||
expect($payload['git_branch'])->toBe('release/canary');
|
||||
expect($payload['build_pack'])->toBe('nixpacks');
|
||||
expect($payload['build_pack'])->toBe('static');
|
||||
expect($payload['ports_exposes'])->toBe('80');
|
||||
expect($payload['install_command'])->toBe('npm ci');
|
||||
expect($payload['build_command'])->toBe('npm run build');
|
||||
expect($payload['publish_directory'])->toBe('dist');
|
||||
expect($payload['is_static'])->toBeTrue();
|
||||
expect($payload['is_spa'])->toBeTrue();
|
||||
expect($payload['domains'])->toBe('https://canary.example.test');
|
||||
expect($payload)->not->toHaveKey('docker_compose_raw');
|
||||
});
|
||||
|
||||
it('updates existing frontend Coolify applications away from legacy Nixpacks detection', function (): void {
|
||||
$manager = new release_manager();
|
||||
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationUpdatePayload');
|
||||
$payloadMethod->setAccessible(true);
|
||||
|
||||
$payload = $payloadMethod->invoke($manager, [
|
||||
'channel_slug' => 'internal',
|
||||
'app' => 'frontend',
|
||||
'repository' => 'copenhagentruckwash/pleno-vue',
|
||||
'branch' => 'master',
|
||||
'commit_sha' => '1132c8c2560e44478d1bb777c88c762a5e1d0b20',
|
||||
], [
|
||||
'coolify_build_pack' => 'nixpacks',
|
||||
]);
|
||||
|
||||
expect($payload['git_repository'])->toBe('copenhagentruckwash/pleno-vue');
|
||||
expect($payload['git_branch'])->toBe('master');
|
||||
expect($payload['git_commit_sha'])->toBe('1132c8c2560e44478d1bb777c88c762a5e1d0b20');
|
||||
expect($payload['build_pack'])->toBe('static');
|
||||
expect($payload['ports_exposes'])->toBe('80');
|
||||
expect($payload['install_command'])->toBe('npm ci');
|
||||
expect($payload['build_command'])->toBe('npm run build');
|
||||
expect($payload['publish_directory'])->toBe('dist');
|
||||
expect($payload['is_static'])->toBeTrue();
|
||||
expect($payload['is_spa'])->toBeTrue();
|
||||
});
|
||||
|
||||
it('can use the Coolify instance default GitHub App when source targets do not store it yet', function (): void {
|
||||
$manager = new release_manager();
|
||||
$payloadMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyApplicationPayload');
|
||||
$payloadMethod->setAccessible(true);
|
||||
|
||||
$payload = $payloadMethod->invoke($manager, [
|
||||
'channel_slug' => 'internal',
|
||||
'app' => 'api',
|
||||
'repository' => 'copenhagentruckwash/api',
|
||||
'branch' => 'master',
|
||||
], [
|
||||
'coolify_project_uuid' => 'project-internal',
|
||||
'coolify_deploy_now' => true,
|
||||
], [
|
||||
'default_environment_name' => 'production',
|
||||
'default_server_uuid' => 'server-default',
|
||||
'default_github_app_uuid' => 'github-app-copenhagentruckwash-github',
|
||||
]);
|
||||
|
||||
expect($payload['github_app_uuid'])->toBe('github-app-copenhagentruckwash-github');
|
||||
expect($payload['git_repository'])->toBe('copenhagentruckwash/api');
|
||||
expect($payload['build_pack'])->toBe('dockerfile');
|
||||
expect($payload['ports_exposes'])->toBe('80');
|
||||
expect($payload)->not->toHaveKey('docker_compose_raw');
|
||||
});
|
||||
|
||||
it('does not treat an existing Coolify service as an application just because a GitHub App UUID is stored', function (): void {
|
||||
$manager = new release_manager();
|
||||
$resourceTypeMethod = new ReflectionMethod(release_manager::class, 'releaseCoolifyResourceType');
|
||||
@@ -324,6 +383,7 @@ it('defines release manager schema, routes, permissions, and system-status integ
|
||||
expect($route)->toContain('/superuser/releases/channels');
|
||||
expect($route)->toContain('/superuser/releases/assignments');
|
||||
expect($route)->toContain('/superuser/releases/service-sets');
|
||||
expect($route)->toContain("\$this->delete('/superuser/releases/service-sets/{id}'");
|
||||
expect($route)->toContain('/superuser/releases/service-sets/{id}/isolated-data-services');
|
||||
expect($route)->toContain('/superuser/releases/bundles');
|
||||
expect($route)->toContain('/superuser/releases/bundles/{id}/deploy');
|
||||
@@ -355,11 +415,19 @@ it('defines release manager schema, routes, permissions, and system-status integ
|
||||
expect($manager)->toContain('commit_mode');
|
||||
expect($manager)->toContain('restartCoolifyService');
|
||||
expect($manager)->toContain('deployCoolifyReleaseTarget');
|
||||
expect($manager)->toContain('deployResource');
|
||||
expect($manager)->toContain('releaseCoolifyGitCommitSha');
|
||||
expect($manager)->toContain('releaseCoolifyForceRebuild');
|
||||
expect($manager)->toContain('listServiceSets');
|
||||
expect($manager)->toContain('createServiceSet');
|
||||
expect($manager)->toContain('deleteServiceSet');
|
||||
expect($manager)->toContain('createBundle');
|
||||
expect($manager)->toContain('deployBundle');
|
||||
expect($manager)->toContain('promoteBundle');
|
||||
expect($manager)->toContain("status = 'superseded'");
|
||||
expect($manager)->toContain('serviceSetIsActive');
|
||||
expect($manager)->toContain('bundleIsActive');
|
||||
expect($manager)->toContain('service_set_removed');
|
||||
expect($manager)->toContain('clone_replica_from_source');
|
||||
expect($manager)->toContain('register_isolated_empty_service');
|
||||
expect($manager)->toContain('isolated_stack');
|
||||
@@ -378,7 +446,10 @@ it('defines release manager schema, routes, permissions, and system-status integ
|
||||
expect($manager)->toContain('coolify_project_uuid');
|
||||
expect($manager)->toContain('releaseCoolifyServicePayload');
|
||||
expect($manager)->toContain('releaseCoolifyApplicationPayload');
|
||||
expect($manager)->toContain('releaseCoolifyApplicationUpdatePayload');
|
||||
expect($manager)->toContain('createPrivateGithubAppApplication');
|
||||
expect($manager)->toContain('coolifyGithubAppSuggestions');
|
||||
expect($manager)->toContain('coolify_github_apps');
|
||||
expect($manager)->toContain('release_deployment_targets');
|
||||
expect($manager)->toContain('resolveChannel');
|
||||
expect($manager)->toContain('capturePolicyFor');
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace classes {
|
||||
};
|
||||
}
|
||||
|
||||
public function createCustomer($number, $name, $cvr, $email, $phone): object
|
||||
public function createCustomer($number, $name, $cvr, $email, $phone, $mobilePhone = null, $companyInformation = null): object
|
||||
{
|
||||
self::$create_calls[] = [
|
||||
'number' => (int)$number,
|
||||
@@ -109,6 +109,8 @@ namespace classes {
|
||||
'cvr' => (string)$cvr,
|
||||
'email' => (string)$email,
|
||||
'phone' => (int)$phone,
|
||||
'mobile_phone' => $mobilePhone === null ? null : (int)$mobilePhone,
|
||||
'company_information' => $companyInformation,
|
||||
];
|
||||
|
||||
$response = self::$mock_create_response ?? (object)[
|
||||
@@ -127,11 +129,19 @@ namespace classes {
|
||||
class virkdata
|
||||
{
|
||||
public static string $mock_name = 'Mock Company';
|
||||
public static string $mock_address = 'Demo Street 1';
|
||||
public static int $mock_zipcode = 2630;
|
||||
public static string $mock_city = 'Taastrup';
|
||||
public static string $mock_website = 'https://demo.test';
|
||||
|
||||
public function getCompanyInformation($cvr, $endpoint, $data): object
|
||||
{
|
||||
$result = new \stdClass();
|
||||
$result->name = self::$mock_name;
|
||||
$result->address = self::$mock_address;
|
||||
$result->zipcode = self::$mock_zipcode;
|
||||
$result->city = self::$mock_city;
|
||||
$result->website = self::$mock_website;
|
||||
|
||||
return $result;
|
||||
}
|
||||
@@ -426,7 +436,7 @@ namespace {
|
||||
],
|
||||
[
|
||||
'name' => 'Successful registration bootstraps local user before welcome emails',
|
||||
'params' => $baseParams,
|
||||
'params' => array_merge($baseParams, ['contactPhone' => 87654320]),
|
||||
'setup' => static function (): void {
|
||||
\classes\economic::$mock_create_response = (object)[
|
||||
'customerNumber' => 12345678,
|
||||
@@ -440,6 +450,14 @@ namespace {
|
||||
'expected_status' => 201,
|
||||
'assert' => static function (): void {
|
||||
assert_true(count(\classes\economic::$create_calls) === 1, 'Fresh registration must call create exactly once.');
|
||||
assert_true(\classes\economic::$create_calls[0]['phone'] === 12345678, 'Fresh registration must use the company phone as the e-conomic customer phone.');
|
||||
assert_true(\classes\economic::$create_calls[0]['mobile_phone'] === 87654320, 'Fresh registration must pass the contact phone as the e-conomic mobile phone.');
|
||||
$companyInformation = \classes\economic::$create_calls[0]['company_information'];
|
||||
assert_true(is_object($companyInformation), 'Fresh registration must pass CVR company information to e-conomic.');
|
||||
assert_true($companyInformation->address === 'Demo Street 1', 'Fresh registration must pass the CVR address to e-conomic.');
|
||||
assert_true($companyInformation->zipcode === 2630, 'Fresh registration must pass the CVR zipcode to e-conomic.');
|
||||
assert_true($companyInformation->city === 'Taastrup', 'Fresh registration must pass the CVR city to e-conomic.');
|
||||
assert_true($companyInformation->website === 'https://demo.test', 'Fresh registration must pass the CVR website to e-conomic.');
|
||||
assert_true(count(\classes\email::$sent) === 2, 'Fresh registration must send two welcome emails.');
|
||||
assert_true(\objects\users_o::$interaction_log[0] === 'bootstrap:12345678', 'Fresh registration must bootstrap the local user before emails.');
|
||||
assert_true(\classes\email::$sent[0]['email'] === 'jm@truckwash.dk', 'Fresh registration should notify the internal welcome recipient first.');
|
||||
@@ -473,6 +491,10 @@ namespace {
|
||||
\classes\economic::reset();
|
||||
\classes\email::reset();
|
||||
\classes\virkdata::$mock_name = 'Mock Company';
|
||||
\classes\virkdata::$mock_address = 'Demo Street 1';
|
||||
\classes\virkdata::$mock_zipcode = 2630;
|
||||
\classes\virkdata::$mock_city = 'Taastrup';
|
||||
\classes\virkdata::$mock_website = 'https://demo.test';
|
||||
\objects\users_o::reset();
|
||||
\objects\logs_o::reset();
|
||||
|
||||
|
||||
@@ -85,6 +85,13 @@ Authorization: Bearer e9d4359673de64f6a9bc4231bf50ac9f5726ad5b3ee6fcad5f8ebbb206
|
||||
"to_date": "2026-04-30",
|
||||
"closed_at": "2026-04-30"
|
||||
}
|
||||
|
||||
### GET request to https://api.truckwash.io:4433/superuser/invoicing/period?dateFrom=2026-05-12&dateTo=2026-05-12&periodView=all&page=1&limit=100&search=&includeRequiresAction=1&includeBooked=1
|
||||
GET https://api.truckwash.io/superuser/invoicing/period?dateFrom=2026-04-01&dateTo=2026-04-30&periodView=all&page=1&limit=all&search=&includeRequiresAction=1&includeBooked=1
|
||||
Accept: application/json
|
||||
Authorization: Bearer e9d4359673de64f6a9bc4231bf50ac9f5726ad5b3ee6fcad5f8ebbb20647c3b8
|
||||
Content-Type: application/json
|
||||
|
||||
### GET request to /subusers/setup
|
||||
GET https://api.truckwash.dk:4433/subusers/setup?token=1c1be8280bac3937487e5c77b76bb839
|
||||
Accept: application/json
|
||||
|
||||
Reference in New Issue
Block a user