1
0

Merge pull request #1004 from bigcapitalhq/fix/pdf-template-logo-display

fix(server): PDF template logo not showing on reopen
This commit is contained in:
Ahmed Bouhuolia
2026-03-01 05:24:54 +02:00
committed by GitHub
15 changed files with 121 additions and 48 deletions
@@ -3,7 +3,7 @@ import * as multerS3 from 'multer-s3';
import { S3_CLIENT, S3Module } from "../S3/S3.module";
import { DeleteAttachment } from "./DeleteAttachment";
import { GetAttachment } from "./GetAttachment";
import { getAttachmentPresignedUrl } from "./GetAttachmentPresignedUrl";
import { GetAttachmentPresignedUrl } from "./GetAttachmentPresignedUrl";
import { LinkAttachment } from "./LinkAttachment";
import { UnlinkAttachment } from "./UnlinkAttachment";
import { ValidateAttachments } from "./ValidateAttachments";
@@ -35,12 +35,12 @@ const models = [
@Module({
imports: [S3Module, ...models],
exports: [...models],
exports: [...models, GetAttachmentPresignedUrl],
controllers: [AttachmentsController],
providers: [
DeleteAttachment,
GetAttachment,
getAttachmentPresignedUrl,
GetAttachmentPresignedUrl,
LinkAttachment,
UnlinkAttachment,
ValidateAttachments,
@@ -4,7 +4,7 @@ import { DeleteAttachment } from './DeleteAttachment';
import { GetAttachment } from './GetAttachment';
import { LinkAttachment } from './LinkAttachment';
import { UnlinkAttachment } from './UnlinkAttachment';
import { getAttachmentPresignedUrl } from './GetAttachmentPresignedUrl';
import { GetAttachmentPresignedUrl } from './GetAttachmentPresignedUrl';
@Injectable()
export class AttachmentsApplication {
@@ -14,7 +14,7 @@ export class AttachmentsApplication {
private readonly getDocumentService: GetAttachment,
private readonly linkDocumentService: LinkAttachment,
private readonly unlinkDocumentService: UnlinkAttachment,
private readonly getPresignedUrlService: getAttachmentPresignedUrl,
private readonly getPresignedUrlService: GetAttachmentPresignedUrl,
) {}
/**
@@ -1,13 +1,13 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { DocumentModel } from './models/Document.model';
import { ConfigService } from '@nestjs/config';
import { S3_CLIENT } from '../S3/S3.module';
@Injectable()
export class getAttachmentPresignedUrl {
export class GetAttachmentPresignedUrl {
constructor(
private readonly configService: ConfigService,
@@ -1,10 +0,0 @@
import * as path from 'path';
// import config from '@/config';
export const getUploadedObjectUri = (objectKey: string) => {
return '';
// return new URL(
// path.join(config.s3.bucket, objectKey),
// config.s3.endpoint
// ).toString();
};
@@ -15,6 +15,8 @@ import { OrganizationBaseCurrencyLocking } from './Organization/OrganizationBase
import { SyncSystemUserToTenantService } from './commands/SyncSystemUserToTenant.service';
import { SyncSystemUserToTenantSubscriber } from './subscribers/SyncSystemUserToTenant.subscriber';
import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob.service';
import { AttachmentsModule } from '../Attachments/Attachment.module';
import { TransformerModule } from '../Transformer/Transformer.module';
@Module({
providers: [
@@ -36,6 +38,8 @@ import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob
adapter: BullMQAdapter,
}),
TenantDBManagerModule,
AttachmentsModule,
TransformerModule,
],
controllers: [OrganizationController],
})
@@ -79,6 +79,13 @@ export class OrganizationMetadataResponseDto {
})
logoKey: string;
@ApiPropertyOptional({
description: 'Logo URL (presigned or public) for display',
example: 'https://...',
nullable: true,
})
logoUri: string;
@ApiPropertyOptional({
description: 'Organization address details',
example: '123 Main St, New York, NY',
@@ -3,14 +3,20 @@ import { throwIfTenantNotExists } from '../Organization/_utils';
import { TenantModel } from '@/modules/System/models/TenantModel';
import { Injectable } from '@nestjs/common';
import { ModelObject } from 'objection';
import { GetAttachmentPresignedUrl } from '@/modules/Attachments/GetAttachmentPresignedUrl';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { GetCurrentOrganizationTransformer } from './GetCurrentOrganization.transformer';
@Injectable()
export class GetCurrentOrganizationService {
constructor(private readonly tenancyContext: TenancyContext) {}
constructor(
private readonly tenancyContext: TenancyContext,
private readonly getPresignedUrlService: GetAttachmentPresignedUrl,
private readonly transformer: TransformerInjectable,
) {}
/**
* Retrieve the current organization metadata.
* @param {number} tenantId
* @returns {Promise<ITenant[]>}
*/
async getCurrentOrganization(): Promise<ModelObject<TenantModel>> {
@@ -21,6 +27,13 @@ export class GetCurrentOrganizationService {
throwIfTenantNotExists(tenant);
return tenant;
const logoUri = tenant.metadata?.logoKey ?
await this.getPresignedUrlService.getPresignedUrl(tenant.metadata.logoKey) : null;
return await this.transformer.transform(
tenant,
new GetCurrentOrganizationTransformer(),
{ logoUri },
);
}
}
@@ -0,0 +1,21 @@
import { Transformer } from '@/modules/Transformer/Transformer';
import { GetCurrentOrganizationMetadataTransformer } from './GetCurrentOrganizationMetadata.transformer';
export class GetCurrentOrganizationTransformer extends Transformer {
/**
* Transforms the tenant/organization for the current-organization response.
* Delegates metadata transformation to GetCurrentOrganizationMetadataTransformer
* and injects the pre-computed logoUri from options.
*/
transform = (tenant: Record<string, any>) => {
const metadataTransformer = new GetCurrentOrganizationMetadataTransformer();
const transformedMetadata = this.item(tenant.metadata, metadataTransformer, {
logoUri: this.options?.logoUri,
});
return {
...tenant,
metadata: transformedMetadata,
};
};
}
@@ -0,0 +1,21 @@
import { Transformer } from '@/modules/Transformer/Transformer';
export class GetCurrentOrganizationMetadataTransformer extends Transformer {
/**
* Include these attributes in the metadata response.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['logoUri'];
};
/**
* Logo URI (presigned or public URL) for display.
* Provided via options from the service after resolving logoKey.
* @param metadata
* @returns {string | null}
*/
public logoUri = (metadata: Record<string, any>): string | null => {
return this.options?.logoUri ?? null;
};
}
@@ -13,6 +13,7 @@ import { BrandingTemplateDTOTransformer } from './BrandingTemplateDTOTransformer
import { GetOrganizationBrandingAttributesService } from './queries/GetOrganizationBrandingAttributes.service';
import { GetPdfTemplates } from './queries/GetPdfTemplates.service';
import { GetPdfTemplateBrandingState } from './queries/GetPdfTemplateBrandingState.service';
import { AttachmentsModule } from '../Attachments/Attachment.module';
@Module({
exports: [
@@ -20,7 +21,7 @@ import { GetPdfTemplateBrandingState } from './queries/GetPdfTemplateBrandingSta
BrandingTemplateDTOTransformer,
GetOrganizationBrandingAttributesService,
],
imports: [TenancyDatabaseModule],
imports: [TenancyDatabaseModule, AttachmentsModule],
controllers: [PdfTemplatesController],
providers: [
PdfTemplateApplication,
@@ -56,16 +56,6 @@ export class PdfTemplateModel extends TenantBaseModel {
return ['companyLogoUri'];
}
/**
* Retrieves the company logo uri.
* @returns {string}
*/
// get companyLogoUri() {
// return this.attributes?.companyLogoKey
// ? getUploadedObjectUri(this.attributes.companyLogoKey)
// : '';
// }
/**
* Relationship mapping.
*/
@@ -1,10 +1,14 @@
import { Injectable } from '@nestjs/common';
import { CommonOrganizationBrandingAttributes } from '../types';
import { TenancyContext } from '../../Tenancy/TenancyContext.service';
import { GetAttachmentPresignedUrl } from '@/modules/Attachments/GetAttachmentPresignedUrl';
@Injectable()
export class GetOrganizationBrandingAttributesService {
constructor(private readonly tenancyContext: TenancyContext) {}
constructor(
private readonly tenancyContext: TenancyContext,
private readonly getPresignedUrlService: GetAttachmentPresignedUrl,
) {}
/**
* Retrieves the given organization branding attributes initial state.
@@ -17,13 +21,22 @@ export class GetOrganizationBrandingAttributesService {
const companyName = tenantMetadata?.name;
const primaryColor = tenantMetadata?.primaryColor;
const companyLogoKey = tenantMetadata?.logoKey;
const companyLogoUri = tenantMetadata?.logoUri;
const companyAddress = tenantMetadata?.addressTextFormatted;
let companyLogoUri: string | null = null;
if (companyLogoKey) {
try {
companyLogoUri =
await this.getPresignedUrlService.getPresignedUrl(companyLogoKey);
} catch {
companyLogoUri = null;
}
}
return {
companyName,
companyAddress,
companyLogoUri,
companyLogoUri: companyLogoUri ?? undefined,
companyLogoKey,
primaryColor,
};
@@ -4,6 +4,7 @@ import { GetPdfTemplateTransformer } from './GetPdfTemplate.transformer';
import { PdfTemplateModel } from '../models/PdfTemplate';
import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { GetAttachmentPresignedUrl } from '@/modules/Attachments/GetAttachmentPresignedUrl';
@Injectable()
export class GetPdfTemplateService {
@@ -13,6 +14,7 @@ export class GetPdfTemplateService {
typeof PdfTemplateModel
>,
private readonly transformer: TransformerInjectable,
private readonly getPresignedUrlService: GetAttachmentPresignedUrl,
) {}
/**
@@ -29,8 +31,19 @@ export class GetPdfTemplateService {
.findById(templateId)
.throwIfNotFound();
const companyLogoKey = template.attributes?.companyLogoKey;
let companyLogoUri: string | null = null;
if (companyLogoKey) {
try {
companyLogoUri =
await this.getPresignedUrlService.getPresignedUrl(companyLogoKey);
} catch {
companyLogoUri = null;
}
}
return this.transformer.transform(
template,
{ ...template, companyLogoUri },
new GetPdfTemplateTransformer(),
);
}
@@ -7,7 +7,7 @@ export class GetPdfTemplateTransformer extends Transformer {
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['createdAtFormatted', 'resourceFormatted', 'attributes'];
return ['createdAtFormatted', 'resourceFormatted', 'attributes', 'companyLogoUri'];
};
/**
@@ -28,6 +28,15 @@ export class GetPdfTemplateTransformer extends Transformer {
// return getTransactionTypeLabel(template.resource);
};
/**
* Retrieves the company logo URI.
* @param {Object} template
* @returns {string | null}
*/
protected companyLogoUri = (template) => {
return template.companyLogoUri;
};
/**
* Retrieves transformed brand attributes.
* @param {} template
@@ -4,7 +4,6 @@ import {
organizationAddressTextFormat,
} from '@/utils/address-text-format';
import { findByIsoCountryCode } from '@bigcapital/utils';
// import { getUploadedObjectUri } from '../../services/Attachments/utils';
export class TenantMetadata extends BaseModel {
public baseCurrency!: string;
@@ -65,17 +64,9 @@ export class TenantMetadata extends BaseModel {
}
/**
* Organization logo url.
* @returns {string | null}
* Retrieves the organization address formatted text.
* @returns {string}
*/
// public get logoUri() {
// return this.logoKey ? getUploadedObjectUri(this.logoKey) : null;
// }
// /**
// * Retrieves the organization address formatted text.
// * @returns {string}
// */
public get addressTextFormatted() {
const addressCountry = findByIsoCountryCode(this.location);