tychat-contracts 1.6.58 → 1.6.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/anamneses/anamnesis-token-session.dto.d.ts +3 -0
- package/dist/anamneses/anamnesis-token-session.dto.d.ts.map +1 -1
- package/dist/anamneses/anamnesis-token-session.dto.js +15 -0
- package/dist/anamneses/normalize-anamnesis-access-token.d.ts +9 -0
- package/dist/anamneses/normalize-anamnesis-access-token.d.ts.map +1 -0
- package/dist/anamneses/normalize-anamnesis-access-token.js +21 -0
- package/dist/anamneses/normalize-anamnesis-access-token.spec.d.ts +2 -0
- package/dist/anamneses/normalize-anamnesis-access-token.spec.d.ts.map +1 -0
- package/dist/anamneses/normalize-anamnesis-access-token.spec.js +18 -0
- package/dist/tenants/anamnesis-public-frontend-url.d.ts +10 -0
- package/dist/tenants/anamnesis-public-frontend-url.d.ts.map +1 -0
- package/dist/tenants/anamnesis-public-frontend-url.js +56 -0
- package/dist/tenants/anamnesis-public-frontend-url.spec.d.ts +2 -0
- package/dist/tenants/anamnesis-public-frontend-url.spec.d.ts.map +1 -0
- package/dist/tenants/anamnesis-public-frontend-url.spec.js +23 -0
- package/dist/tenants/index.d.ts +1 -0
- package/dist/tenants/index.d.ts.map +1 -1
- package/dist/tenants/index.js +1 -0
- package/package.json +1 -1
- package/src/anamneses/anamnesis-token-session.dto.ts +15 -0
- package/src/patients/create-patient.dto.ts +20 -0
- package/src/tenants/anamnesis-public-frontend-url.spec.ts +45 -0
- package/src/tenants/anamnesis-public-frontend-url.ts +61 -0
- package/src/tenants/index.ts +1 -0
|
@@ -4,6 +4,9 @@ export declare class AnamnesisTokenSessionPatientDto {
|
|
|
4
4
|
name: string;
|
|
5
5
|
cpf: string | null;
|
|
6
6
|
phone: string | null;
|
|
7
|
+
birthDate: string | null;
|
|
8
|
+
email: string | null;
|
|
9
|
+
address: string | null;
|
|
7
10
|
}
|
|
8
11
|
export declare class AnamnesisTokenSessionDto {
|
|
9
12
|
valid: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anamnesis-token-session.dto.d.ts","sourceRoot":"","sources":["../../src/anamneses/anamnesis-token-session.dto.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,qBAAa,+BAA+B;IAE1C,EAAE,EAAE,MAAM,CAAC;IAGX,IAAI,EAAE,MAAM,CAAC;IAGb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAGnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"anamnesis-token-session.dto.d.ts","sourceRoot":"","sources":["../../src/anamneses/anamnesis-token-session.dto.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,qBAAa,+BAA+B;IAE1C,EAAE,EAAE,MAAM,CAAC;IAGX,IAAI,EAAE,MAAM,CAAC;IAGb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAGnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAGrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAGzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAGrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,qBAAa,wBAAwB;IAEnC,KAAK,EAAE,OAAO,CAAC;IAMf,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,eAAe,CAAC;IAGhE,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,aAAa,CAAC,EAAE,MAAM,CAAC;IAGvB,OAAO,CAAC,EAAE,+BAA+B,CAAC;IAM1C,qBAAqB,CAAC,EAAE,uBAAuB,EAAE,CAAC;CACnD"}
|
|
@@ -17,6 +17,9 @@ class AnamnesisTokenSessionPatientDto {
|
|
|
17
17
|
name;
|
|
18
18
|
cpf;
|
|
19
19
|
phone;
|
|
20
|
+
birthDate;
|
|
21
|
+
email;
|
|
22
|
+
address;
|
|
20
23
|
}
|
|
21
24
|
exports.AnamnesisTokenSessionPatientDto = AnamnesisTokenSessionPatientDto;
|
|
22
25
|
__decorate([
|
|
@@ -35,6 +38,18 @@ __decorate([
|
|
|
35
38
|
(0, swagger_1.ApiPropertyOptional)({ example: '+5511999999999', nullable: true }),
|
|
36
39
|
__metadata("design:type", Object)
|
|
37
40
|
], AnamnesisTokenSessionPatientDto.prototype, "phone", void 0);
|
|
41
|
+
__decorate([
|
|
42
|
+
(0, swagger_1.ApiPropertyOptional)({ example: '1990-05-12', nullable: true }),
|
|
43
|
+
__metadata("design:type", Object)
|
|
44
|
+
], AnamnesisTokenSessionPatientDto.prototype, "birthDate", void 0);
|
|
45
|
+
__decorate([
|
|
46
|
+
(0, swagger_1.ApiPropertyOptional)({ example: 'maria@email.com', nullable: true }),
|
|
47
|
+
__metadata("design:type", Object)
|
|
48
|
+
], AnamnesisTokenSessionPatientDto.prototype, "email", void 0);
|
|
49
|
+
__decorate([
|
|
50
|
+
(0, swagger_1.ApiPropertyOptional)({ example: 'Rua Exemplo, 123 — Centro', nullable: true }),
|
|
51
|
+
__metadata("design:type", Object)
|
|
52
|
+
], AnamnesisTokenSessionPatientDto.prototype, "address", void 0);
|
|
38
53
|
class AnamnesisTokenSessionDto {
|
|
39
54
|
valid;
|
|
40
55
|
reason;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Hex length of `randomBytes(32).toString('hex')` anamnesis tokens. */
|
|
2
|
+
export declare const ANAMNESIS_ACCESS_TOKEN_HEX_LENGTH = 64;
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes a raw token from URL/query/WhatsApp linkification.
|
|
5
|
+
* Meta templates often place punctuation right after {{1}} (e.g. "{{1}}."),
|
|
6
|
+
* which mobile clients include in the tapped URL and breaks SHA-256 lookup.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeAnamnesisAccessToken(raw: string | null | undefined): string;
|
|
9
|
+
//# sourceMappingURL=normalize-anamnesis-access-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize-anamnesis-access-token.d.ts","sourceRoot":"","sources":["../../src/anamneses/normalize-anamnesis-access-token.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,eAAO,MAAM,iCAAiC,KAAK,CAAC;AAEpD;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAYpF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ANAMNESIS_ACCESS_TOKEN_HEX_LENGTH = void 0;
|
|
4
|
+
exports.normalizeAnamnesisAccessToken = normalizeAnamnesisAccessToken;
|
|
5
|
+
/** Hex length of `randomBytes(32).toString('hex')` anamnesis tokens. */
|
|
6
|
+
exports.ANAMNESIS_ACCESS_TOKEN_HEX_LENGTH = 64;
|
|
7
|
+
/**
|
|
8
|
+
* Normalizes a raw token from URL/query/WhatsApp linkification.
|
|
9
|
+
* Meta templates often place punctuation right after {{1}} (e.g. "{{1}}."),
|
|
10
|
+
* which mobile clients include in the tapped URL and breaks SHA-256 lookup.
|
|
11
|
+
*/
|
|
12
|
+
function normalizeAnamnesisAccessToken(raw) {
|
|
13
|
+
const trimmed = (raw ?? '').trim();
|
|
14
|
+
if (!trimmed)
|
|
15
|
+
return '';
|
|
16
|
+
const exact = trimmed.match(new RegExp(`^([a-fA-F0-9]{${exports.ANAMNESIS_ACCESS_TOKEN_HEX_LENGTH}})`));
|
|
17
|
+
if (exact?.[1]) {
|
|
18
|
+
return exact[1].toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
return trimmed.replace(/[.,;:!?)>\]"']+$/g, '').trim();
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize-anamnesis-access-token.spec.d.ts","sourceRoot":"","sources":["../../src/anamneses/normalize-anamnesis-access-token.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const normalize_anamnesis_access_token_1 = require("./normalize-anamnesis-access-token");
|
|
4
|
+
const TOKEN = '22d58c2cd36a147d464d1525cfd45657bd42491276bdc9ae5bef580d70b57168';
|
|
5
|
+
describe('normalizeAnamnesisAccessToken', () => {
|
|
6
|
+
it('returns valid 64-char hex token unchanged (lowercased)', () => {
|
|
7
|
+
expect((0, normalize_anamnesis_access_token_1.normalizeAnamnesisAccessToken)(TOKEN)).toBe(TOKEN);
|
|
8
|
+
});
|
|
9
|
+
it('strips trailing period from WhatsApp linkification', () => {
|
|
10
|
+
expect((0, normalize_anamnesis_access_token_1.normalizeAnamnesisAccessToken)(`${TOKEN}.`)).toBe(TOKEN);
|
|
11
|
+
});
|
|
12
|
+
it('strips trailing punctuation cluster', () => {
|
|
13
|
+
expect((0, normalize_anamnesis_access_token_1.normalizeAnamnesisAccessToken)(`${TOKEN}).`)).toBe(TOKEN);
|
|
14
|
+
});
|
|
15
|
+
it('returns empty for blank input', () => {
|
|
16
|
+
expect((0, normalize_anamnesis_access_token_1.normalizeAnamnesisAccessToken)(' ')).toBe('');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** True when the hostname's first label matches the tenant slug segment. */
|
|
2
|
+
export declare function hostnameMatchesTenantSlug(hostname: string, tenantSlug: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Builds a frontend base URL that resolves to a single tenant on public routes.
|
|
5
|
+
*
|
|
6
|
+
* Shared hosts (e.g. `qa.tychat.app`) are scoped as `{slug}.qa.tychat.app`.
|
|
7
|
+
* Custom domains and tenant-specific `*.tychat.app` hosts are kept as configured.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveTenantScopedFrontendBaseUrl(tenantSlug: string, configuredHostRaw: string | null | undefined): string;
|
|
10
|
+
//# sourceMappingURL=anamnesis-public-frontend-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anamnesis-public-frontend-url.d.ts","sourceRoot":"","sources":["../../src/tenants/anamnesis-public-frontend-url.ts"],"names":[],"mappings":"AAiBA,4EAA4E;AAC5E,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAIvF;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAChD,UAAU,EAAE,MAAM,EAClB,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC3C,MAAM,CA2BR"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hostnameMatchesTenantSlug = hostnameMatchesTenantSlug;
|
|
4
|
+
exports.resolveTenantScopedFrontendBaseUrl = resolveTenantScopedFrontendBaseUrl;
|
|
5
|
+
const tenant_slug_util_1 = require("./tenant-slug.util");
|
|
6
|
+
const TYCHAT_APP_SUFFIX = '.tychat.app';
|
|
7
|
+
function normalizeFrontendBaseUrl(raw) {
|
|
8
|
+
const trimmed = raw.trim().replace(/\/+$/, '');
|
|
9
|
+
if (!trimmed)
|
|
10
|
+
return '';
|
|
11
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
12
|
+
return trimmed;
|
|
13
|
+
}
|
|
14
|
+
return `https://${trimmed}`;
|
|
15
|
+
}
|
|
16
|
+
function firstHostnameLabel(hostname) {
|
|
17
|
+
return hostname.toLowerCase().split('.').filter(Boolean)[0] ?? '';
|
|
18
|
+
}
|
|
19
|
+
/** True when the hostname's first label matches the tenant slug segment. */
|
|
20
|
+
function hostnameMatchesTenantSlug(hostname, tenantSlug) {
|
|
21
|
+
const segment = (0, tenant_slug_util_1.slugToDomainSegment)(tenantSlug);
|
|
22
|
+
if (!segment || segment === 'default')
|
|
23
|
+
return false;
|
|
24
|
+
return firstHostnameLabel(hostname) === segment;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Builds a frontend base URL that resolves to a single tenant on public routes.
|
|
28
|
+
*
|
|
29
|
+
* Shared hosts (e.g. `qa.tychat.app`) are scoped as `{slug}.qa.tychat.app`.
|
|
30
|
+
* Custom domains and tenant-specific `*.tychat.app` hosts are kept as configured.
|
|
31
|
+
*/
|
|
32
|
+
function resolveTenantScopedFrontendBaseUrl(tenantSlug, configuredHostRaw) {
|
|
33
|
+
const slug = tenantSlug?.trim();
|
|
34
|
+
if (!slug)
|
|
35
|
+
return '';
|
|
36
|
+
const segment = (0, tenant_slug_util_1.slugToDomainSegment)(slug);
|
|
37
|
+
const fallback = `${segment}${TYCHAT_APP_SUFFIX}`;
|
|
38
|
+
const configured = configuredHostRaw?.trim();
|
|
39
|
+
const base = normalizeFrontendBaseUrl(configured || fallback);
|
|
40
|
+
let url;
|
|
41
|
+
try {
|
|
42
|
+
url = new URL(base);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return normalizeFrontendBaseUrl(fallback);
|
|
46
|
+
}
|
|
47
|
+
const hostname = url.hostname.toLowerCase();
|
|
48
|
+
if (hostnameMatchesTenantSlug(hostname, slug)) {
|
|
49
|
+
return url.origin;
|
|
50
|
+
}
|
|
51
|
+
if (hostname.endsWith(TYCHAT_APP_SUFFIX)) {
|
|
52
|
+
url.hostname = `${segment}.${hostname}`;
|
|
53
|
+
return url.origin;
|
|
54
|
+
}
|
|
55
|
+
return url.origin;
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anamnesis-public-frontend-url.spec.d.ts","sourceRoot":"","sources":["../../src/tenants/anamnesis-public-frontend-url.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const anamnesis_public_frontend_url_1 = require("./anamnesis-public-frontend-url");
|
|
4
|
+
describe('resolveTenantScopedFrontendBaseUrl', () => {
|
|
5
|
+
it('keeps tenant-specific tychat.app host', () => {
|
|
6
|
+
expect((0, anamnesis_public_frontend_url_1.resolveTenantScopedFrontendBaseUrl)('clinica_sampaio', 'https://clinica-sampaio.tychat.app')).toBe('https://clinica-sampaio.tychat.app');
|
|
7
|
+
});
|
|
8
|
+
it('scopes shared QA host with tenant subdomain', () => {
|
|
9
|
+
expect((0, anamnesis_public_frontend_url_1.resolveTenantScopedFrontendBaseUrl)('clinica_sampaio', 'https://qa.tychat.app')).toBe('https://clinica-sampaio.qa.tychat.app');
|
|
10
|
+
});
|
|
11
|
+
it('keeps custom domain unchanged', () => {
|
|
12
|
+
expect((0, anamnesis_public_frontend_url_1.resolveTenantScopedFrontendBaseUrl)('clinica_sampaio', 'https://app.minhaclinica.com.br')).toBe('https://app.minhaclinica.com.br');
|
|
13
|
+
});
|
|
14
|
+
it('uses slug fallback when host is empty', () => {
|
|
15
|
+
expect((0, anamnesis_public_frontend_url_1.resolveTenantScopedFrontendBaseUrl)('homolog', '')).toBe('https://homolog.tychat.app');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe('hostnameMatchesTenantSlug', () => {
|
|
19
|
+
it('matches first DNS label to slug segment', () => {
|
|
20
|
+
expect((0, anamnesis_public_frontend_url_1.hostnameMatchesTenantSlug)('clinica-sampaio.qa.tychat.app', 'clinica_sampaio')).toBe(true);
|
|
21
|
+
expect((0, anamnesis_public_frontend_url_1.hostnameMatchesTenantSlug)('qa.tychat.app', 'clinica_sampaio')).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
});
|
package/dist/tenants/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './get-tenant-by-address.query.dto';
|
|
|
3
3
|
export * from './tenant-public-by-address-response.dto';
|
|
4
4
|
export * from './whatsapp-provider-kind.dto';
|
|
5
5
|
export * from './tenant-slug.util';
|
|
6
|
+
export * from './anamnesis-public-frontend-url';
|
|
6
7
|
export * from './tenant-ai-token-state.dto';
|
|
7
8
|
export * from './consume-tenant-ai-tokens.dto';
|
|
8
9
|
export * from './recharge-tenant-ai-tokens.dto';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tenants/index.ts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,mCAAmC,CAAC;AAClD,cAAc,yCAAyC,CAAC;AACxD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wCAAwC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tenants/index.ts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,mCAAmC,CAAC;AAClD,cAAc,yCAAyC,CAAC;AACxD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wCAAwC,CAAC"}
|
package/dist/tenants/index.js
CHANGED
|
@@ -19,6 +19,7 @@ __exportStar(require("./get-tenant-by-address.query.dto"), exports);
|
|
|
19
19
|
__exportStar(require("./tenant-public-by-address-response.dto"), exports);
|
|
20
20
|
__exportStar(require("./whatsapp-provider-kind.dto"), exports);
|
|
21
21
|
__exportStar(require("./tenant-slug.util"), exports);
|
|
22
|
+
__exportStar(require("./anamnesis-public-frontend-url"), exports);
|
|
22
23
|
__exportStar(require("./tenant-ai-token-state.dto"), exports);
|
|
23
24
|
__exportStar(require("./consume-tenant-ai-tokens.dto"), exports);
|
|
24
25
|
__exportStar(require("./recharge-tenant-ai-tokens.dto"), exports);
|
package/package.json
CHANGED
|
@@ -13,6 +13,21 @@ export class AnamnesisTokenSessionPatientDto {
|
|
|
13
13
|
|
|
14
14
|
@ApiPropertyOptional({ example: '+5511999999999', nullable: true })
|
|
15
15
|
phone: string | null;
|
|
16
|
+
|
|
17
|
+
@ApiPropertyOptional({ example: '12.345.678-9', nullable: true })
|
|
18
|
+
rg: string | null;
|
|
19
|
+
|
|
20
|
+
@ApiPropertyOptional({ example: '1990-05-12', nullable: true })
|
|
21
|
+
birthDate: string | null;
|
|
22
|
+
|
|
23
|
+
@ApiPropertyOptional({ example: 'maria@email.com', nullable: true })
|
|
24
|
+
email: string | null;
|
|
25
|
+
|
|
26
|
+
@ApiPropertyOptional({ example: 'Rua Exemplo, 123 — Centro', nullable: true })
|
|
27
|
+
address: string | null;
|
|
28
|
+
|
|
29
|
+
@ApiPropertyOptional({ example: '+5584999999999', nullable: true })
|
|
30
|
+
emergencyContact: string | null;
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
export class AnamnesisTokenSessionDto {
|
|
@@ -59,6 +59,16 @@ export class CreatePatientDto {
|
|
|
59
59
|
@MaxLength(30)
|
|
60
60
|
phone?: string | null;
|
|
61
61
|
|
|
62
|
+
@ApiPropertyOptional({
|
|
63
|
+
description: 'RG do paciente',
|
|
64
|
+
example: '12.345.678-9',
|
|
65
|
+
maxLength: 20,
|
|
66
|
+
})
|
|
67
|
+
@IsString()
|
|
68
|
+
@IsOptional()
|
|
69
|
+
@MaxLength(20)
|
|
70
|
+
rg?: string | null;
|
|
71
|
+
|
|
62
72
|
@ApiPropertyOptional({
|
|
63
73
|
description: 'Gênero do paciente',
|
|
64
74
|
example: 'masculino',
|
|
@@ -79,6 +89,16 @@ export class CreatePatientDto {
|
|
|
79
89
|
@MaxLength(500)
|
|
80
90
|
address?: string | null;
|
|
81
91
|
|
|
92
|
+
@ApiPropertyOptional({
|
|
93
|
+
description: 'Contato de emergência do paciente',
|
|
94
|
+
example: '+55 84 99999-9999',
|
|
95
|
+
maxLength: 30,
|
|
96
|
+
})
|
|
97
|
+
@IsString()
|
|
98
|
+
@IsOptional()
|
|
99
|
+
@MaxLength(30)
|
|
100
|
+
emergencyContact?: string | null;
|
|
101
|
+
|
|
82
102
|
@ApiProperty({
|
|
83
103
|
description: 'Indica se o paciente possui alguma deficiência',
|
|
84
104
|
example: true,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hostnameMatchesTenantSlug,
|
|
3
|
+
resolveTenantScopedFrontendBaseUrl,
|
|
4
|
+
} from './anamnesis-public-frontend-url';
|
|
5
|
+
|
|
6
|
+
describe('resolveTenantScopedFrontendBaseUrl', () => {
|
|
7
|
+
it('keeps tenant-specific tychat.app host', () => {
|
|
8
|
+
expect(
|
|
9
|
+
resolveTenantScopedFrontendBaseUrl(
|
|
10
|
+
'clinica_sampaio',
|
|
11
|
+
'https://clinica-sampaio.tychat.app',
|
|
12
|
+
),
|
|
13
|
+
).toBe('https://clinica-sampaio.tychat.app');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('scopes shared QA host with tenant subdomain', () => {
|
|
17
|
+
expect(
|
|
18
|
+
resolveTenantScopedFrontendBaseUrl('clinica_sampaio', 'https://qa.tychat.app'),
|
|
19
|
+
).toBe('https://clinica-sampaio.qa.tychat.app');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('keeps custom domain unchanged', () => {
|
|
23
|
+
expect(
|
|
24
|
+
resolveTenantScopedFrontendBaseUrl(
|
|
25
|
+
'clinica_sampaio',
|
|
26
|
+
'https://app.minhaclinica.com.br',
|
|
27
|
+
),
|
|
28
|
+
).toBe('https://app.minhaclinica.com.br');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('uses slug fallback when host is empty', () => {
|
|
32
|
+
expect(resolveTenantScopedFrontendBaseUrl('homolog', '')).toBe(
|
|
33
|
+
'https://homolog.tychat.app',
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('hostnameMatchesTenantSlug', () => {
|
|
39
|
+
it('matches first DNS label to slug segment', () => {
|
|
40
|
+
expect(hostnameMatchesTenantSlug('clinica-sampaio.qa.tychat.app', 'clinica_sampaio')).toBe(
|
|
41
|
+
true,
|
|
42
|
+
);
|
|
43
|
+
expect(hostnameMatchesTenantSlug('qa.tychat.app', 'clinica_sampaio')).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { slugToDomainSegment } from './tenant-slug.util';
|
|
2
|
+
|
|
3
|
+
const TYCHAT_APP_SUFFIX = '.tychat.app';
|
|
4
|
+
|
|
5
|
+
function normalizeFrontendBaseUrl(raw: string): string {
|
|
6
|
+
const trimmed = raw.trim().replace(/\/+$/, '');
|
|
7
|
+
if (!trimmed) return '';
|
|
8
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
9
|
+
return trimmed;
|
|
10
|
+
}
|
|
11
|
+
return `https://${trimmed}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function firstHostnameLabel(hostname: string): string {
|
|
15
|
+
return hostname.toLowerCase().split('.').filter(Boolean)[0] ?? '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** True when the hostname's first label matches the tenant slug segment. */
|
|
19
|
+
export function hostnameMatchesTenantSlug(hostname: string, tenantSlug: string): boolean {
|
|
20
|
+
const segment = slugToDomainSegment(tenantSlug);
|
|
21
|
+
if (!segment || segment === 'default') return false;
|
|
22
|
+
return firstHostnameLabel(hostname) === segment;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds a frontend base URL that resolves to a single tenant on public routes.
|
|
27
|
+
*
|
|
28
|
+
* Shared hosts (e.g. `qa.tychat.app`) are scoped as `{slug}.qa.tychat.app`.
|
|
29
|
+
* Custom domains and tenant-specific `*.tychat.app` hosts are kept as configured.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveTenantScopedFrontendBaseUrl(
|
|
32
|
+
tenantSlug: string,
|
|
33
|
+
configuredHostRaw: string | null | undefined,
|
|
34
|
+
): string {
|
|
35
|
+
const slug = tenantSlug?.trim();
|
|
36
|
+
if (!slug) return '';
|
|
37
|
+
|
|
38
|
+
const segment = slugToDomainSegment(slug);
|
|
39
|
+
const fallback = `${segment}${TYCHAT_APP_SUFFIX}`;
|
|
40
|
+
const configured = configuredHostRaw?.trim();
|
|
41
|
+
const base = normalizeFrontendBaseUrl(configured || fallback);
|
|
42
|
+
|
|
43
|
+
let url: URL;
|
|
44
|
+
try {
|
|
45
|
+
url = new URL(base);
|
|
46
|
+
} catch {
|
|
47
|
+
return normalizeFrontendBaseUrl(fallback);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const hostname = url.hostname.toLowerCase();
|
|
51
|
+
if (hostnameMatchesTenantSlug(hostname, slug)) {
|
|
52
|
+
return url.origin;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (hostname.endsWith(TYCHAT_APP_SUFFIX)) {
|
|
56
|
+
url.hostname = `${segment}.${hostname}`;
|
|
57
|
+
return url.origin;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return url.origin;
|
|
61
|
+
}
|
package/src/tenants/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './get-tenant-by-address.query.dto';
|
|
|
3
3
|
export * from './tenant-public-by-address-response.dto';
|
|
4
4
|
export * from './whatsapp-provider-kind.dto';
|
|
5
5
|
export * from './tenant-slug.util';
|
|
6
|
+
export * from './anamnesis-public-frontend-url';
|
|
6
7
|
export * from './tenant-ai-token-state.dto';
|
|
7
8
|
export * from './consume-tenant-ai-tokens.dto';
|
|
8
9
|
export * from './recharge-tenant-ai-tokens.dto';
|