saldo
Version:
Portuguese salary calculator library
279 lines (259 loc) • 8.73 kB
text/typescript
export type SituationCodesT =
| "SOLCAS2"
| "SOLD"
| "CAS1"
| "SOLCAS2+DEF"
| "SOLD+DEF"
| "CAS2D+DEF"
| "CAS1+DEF";
export type LocationT = "continente" | "acores" | "madeira";
export interface Condition {
married: boolean;
dependents: boolean; // True if has dependents False if not
disabled: boolean;
description: string;
numberOfHolders?: number | null; // Number of holders, null if it's not applicable (e.g. not married)
// Note: The Python __init__ logic for dependents based on numberOfDependents is handled differently in TS.
// Consumers of this interface will need to ensure 'dependents' is set correctly.
// We can add a constructor or factory function if complex initialization is needed.
}
export interface Situation {
code: SituationCodesT;
description: string;
conditions: Condition[];
}
export const Situations: { [key: string]: Situation } = {
SOLCAS2: {
code: "SOLCAS2",
description:
"Trabalho dependente - Não casado sem dependentes ou casado dois titulares",
conditions: [
{
description: "Não Casado sem dependentes",
married: false,
dependents: false,
disabled: false,
},
{
description: "Casado, 2 titulares, sem dependentes",
married: true,
numberOfHolders: 2,
dependents: false,
disabled: false,
},
{
description: "Casado, 2 titulares, com dependentes",
married: true,
numberOfHolders: 2,
dependents: true,
disabled: false,
},
],
},
SOLD: {
code: "SOLD",
description: "Trabalho dependente - Não casado com um ou mais dependentes",
conditions: [
{
description: "Não casado com um ou mais dependentes",
married: false,
dependents: true,
disabled: false,
},
],
},
CAS1: {
code: "CAS1",
description: "Trabalho dependente - Casado único titular",
conditions: [
{
description: "Casado único titular sem dependentes",
married: true,
numberOfHolders: 1,
dependents: false,
disabled: false,
},
{
description: "Casado único titular com dependentes",
married: true,
numberOfHolders: 1,
dependents: true,
disabled: false,
},
],
},
SOLCAS2_DEF: {
code: "SOLCAS2+DEF",
description:
"Trabalho dependente - Não casado ou casado dois titulares sem dependentes - deficiente",
conditions: [
{
description: "Não Casado sem dependentes - deficiente",
married: false,
dependents: false,
disabled: true,
},
{
description: "Casado, 2 titulares, sem dependentes - deficiente",
married: true,
numberOfHolders: 2,
dependents: false,
disabled: true,
},
],
},
SOLD_DEF: {
code: "SOLD+DEF",
description:
"Trabalho dependente - Não casado, com um ou mais dependentes - deficiente",
conditions: [
{
description: "Não casado com um ou mais dependentes - deficiente",
married: false,
dependents: true,
disabled: true,
},
],
},
CAS2D_DEF: {
code: "CAS2D+DEF",
description:
"Trabalho dependente - Casado dois titulares, com um ou mais dependentes - deficiente",
conditions: [
{
description: "Casado, 2 titulares, com dependentes - deficiente",
married: true,
numberOfHolders: 2,
dependents: true,
disabled: true,
},
],
},
CAS1_DEF: {
code: "CAS1+DEF",
description: "Trabalho dependente - Casado único titular - deficiente",
conditions: [
{
description: "Casado único titular sem dependentes - deficiente",
married: true,
numberOfHolders: 1,
dependents: false, // Python had None, translated to false based on Condition.__init__
disabled: true,
},
{
description: "Casado único titular com dependentes - deficiente",
married: true,
numberOfHolders: 1,
dependents: true,
disabled: true,
},
],
},
};
// Utility functions for Situations
export class SituationUtils {
public static getSituationFromCode(code: string): Situation | undefined {
return Object.keys(Situations)
.map((key) => Situations[key])
.find((situation: Situation) => situation.code === code);
}
public static getSituation(
married: boolean,
disabled: boolean,
numberOfHolders?: number | null,
numberOfDependents?: number
): Situation | undefined {
const inputCondition: Partial<Condition> & {
married: boolean;
disabled: boolean;
dependents: boolean;
} = {
married,
numberOfHolders: numberOfHolders,
dependents:
numberOfDependents !== undefined ? numberOfDependents > 0 : false,
disabled,
};
return SituationUtils.getSituationFromCondition(
inputCondition as Condition
);
}
public static getSituationFromCondition(
condition: Condition
): Situation | undefined {
for (const key in Situations) {
if (Object.prototype.hasOwnProperty.call(Situations, key)) {
const situation = Situations[key];
for (const situationCondition of situation.conditions) {
const marriedMatch = situationCondition.married === condition.married;
let holdersMatch = true;
if (
situationCondition.numberOfHolders !== undefined &&
situationCondition.numberOfHolders !== null
) {
holdersMatch =
situationCondition.numberOfHolders === condition.numberOfHolders;
}
// In Python, if situation_condition.number_of_holders is None, it implies a match.
// Here, if situationCondition.numberOfHolders is undefined/null, we consider it a match only if condition.numberOfHolders is also undefined/null.
// This might need adjustment based on exact Python None semantics vs TypeScript undefined/null.
// For now, if schema has no numberOfHolders, it matches any.
// If schema *has* numberOfHolders, it must match.
let dependentsMatch = true;
if (situationCondition.dependents !== undefined) {
// Python code treated None for dependents as a wildcard / match
dependentsMatch =
situationCondition.dependents === condition.dependents;
}
const disabledMatch =
situationCondition.disabled === condition.disabled;
if (marriedMatch && holdersMatch && dependentsMatch && disabledMatch) {
return situation;
}
}
}
}
// In Python, this raised ValueError. In TS, we return undefined.
// console.error(`Situation with condition ${JSON.stringify(condition)} not found.`);
return undefined;
}
}
export class RetentionPathsSchema {
public identifier: string; // Renamed from yearPath, locationPath, etc. to a single identifier
constructor(
dateStart: Date,
dateEnd: Date,
location: LocationT,
situationCode: SituationCodesT, // This is the code like "SOLD", "CAS1"
year: number | string
) {
const yearStr = String(year);
const customPadStart = (str: string, targetLength: number, padString: string): string => {
targetLength = targetLength >> 0; // floor if number or convert non-number to 0
padString = String(typeof padString !== 'undefined' ? padString : ' ');
if (str.length > targetLength) {
return String(str);
} else {
targetLength = targetLength - str.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length); // append to original to ensure we are longer than needed
}
return padString.slice(0, targetLength) + String(str);
}
};
const formatDate = (date: Date) => {
const y = date.getFullYear();
const m = customPadStart(String(date.getMonth() + 1), 2, "0");
const d = customPadStart(String(date.getDate()), 2, "0");
return `${y}-${m}-${d}`;
};
// Construct the identifier string matching the keys in taxTablesData
// e.g., "2024/continente/2024-01-01_2024-12-31/SOLD"
// Note: situationCode already comes without .json
this.identifier = `${yearStr}/${location}/${formatDate(dateStart)}_${formatDate(dateEnd)}/${situationCode}`;
}
public get path(): string { // Keep 'path' getter name for compatibility if it's widely used,
// but it now returns the identifier.
return this.identifier;
}
}