diff options
Diffstat (limited to 'docs/PROTOCOL.md')
| -rw-r--r-- | docs/PROTOCOL.md | 38 |
1 files changed, 33 insertions, 5 deletions
diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 9a2a3c4..ebce8bf 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -111,7 +111,7 @@ Scopes on a token can narrow but never widen the user's role. | `bank.import`, `bank.match`, `bank.unmatch` | | ● | ● | | | `sie.import`, `account.create`, `account.update` | | ● | ● | | | `period.lock`, `fiscal_year.open/close/reopen`, `org.update` | | | ● | | -| `employee.create/update/archive`, `payroll.run_post`, `payroll.pay_tax`, `payroll.settings_set` | | ● | ● | | +| `employee.create/update/archive`, `payroll.run_post`, `payroll.pay_tax`, `payroll.payslip_mail`, `payroll.settings_set` | | ● | ● | | | `payroll.agi` (decrypted personnummer), `payroll.tax_tables_fetch/import` | | | ● | | | `org.member_*`, `token.create` for others | | | ● | | | `user.create`, any org | | | | ● | @@ -659,13 +659,15 @@ missing, reads return an all-masked `********` instead of failing. |---|---|---| | `employee.list` | `active_only?` | `items[]` ordered by name, personnummer masked | | `employee.get` | `id` | one employee, personnummer masked | -| `employee.create` | `name`, `personal_no`, `address?`, `postal_code?`, `city?`, `bank_account?`, `salary_account?`, `monthly_salary_ore?`, `tax_table?`, `tax_column?` | the employee | +| `employee.create` | `name`, `personal_no`, `address?`, `postal_code?`, `city?`, `bank_account?`, `email?`, `salary_account?`, `monthly_salary_ore?`, `tax_table?`, `tax_column?` | the employee | | `employee.update` | `id` plus any field (merged) | the effective employee | | `employee.archive` | `id`, `active` | `id`, `active` | `salary_account` (digits only) defaults to the `payroll_salary_account` setting (default `7210`), `tax_table` is 29–42 and `tax_column` is 1–6. -Creating a second employee with the same personnummer is a `CONFLICT`. All +`email` (at most 254 characters, no control characters) is the recipient for +`payroll.payslip_mail`. Creating a second employee with the same +personnummer is a `CONFLICT`. All mutations support `dry_run` and are audited (`employee.create`, `employee.update`, `employee.archive`); the audit entry redacts the personnummer. @@ -681,6 +683,8 @@ personnummer. | `payroll.run_post` | `period`, `pay_date`, `dry_run?` | `id`, `voucher_id`, totals; posts the run voucher | | `payroll.run_list` | `limit?` | runs, newest period first | | `payroll.run_get` | `id` | run with `lines[]` | +| `payroll.payslip` | `run_id`, `employee_id?` | one employee's payslip PDF as `content_base64`, `filename` | +| `payroll.payslip_mail` | `run_id`, `employee_id?`, `to?` | sends the payslip; `sent_to`, `at`, `attachment_id`, `filename` | | `payroll.agi` | `period` | per-employee AGI underlag incl. the personnummer in clear (owner) | | `payroll.pay_tax` | `run_id`, `date?`, `dry_run?` | payment voucher; marks the run `paid` | | `payroll.settings_get` | — | effective payroll settings (defaults included) | @@ -726,6 +730,28 @@ only command that reveals the number. `payroll.pay_tax` books given date (default: today) and sets the run's `status` to `paid` with `payment_voucher_id`; paying twice is a `CONFLICT`. +`payroll.payslip` renders one A4 lönebesked for an employee line of a posted +run: the employer header and footer, the employee name, the masked +personnummer (`********-1234`, or `********` when the key is unavailable), +period, pay date, tax table and column, then Bruttolön, the negative +Preliminärskatt, a rule and Nettolön, and the note `Arbetsgivaravgifter +<rate> % betalas av arbetsgivaren.` (from `payroll_avgift_rate_bp`). The +personnummer is never written in clear. `employee_id` may be omitted only +when the run has a single line; an unknown run, employee or line is +`NOT_FOUND`. Nothing is stored. + +`payroll.payslip_mail` renders the same PDF, stores it as an +`application/pdf` attachment named `Lönebesked <period> <name>.pdf`, links +it to the run's voucher with `voucher_attachments` (the run must have one; +otherwise `INTERNAL`) and mails it with subject `Lönebesked <period>` and a +short Swedish body with the net amount. The recipient is `to`, or the +employee's `email`; an empty address is `INVALID_ARGS`. SMTP configuration +follows `invoice.send`; missing configuration is `SMTP_NOT_CONFIGURED` and a +failed delivery `SMTP_FAILED` with the stored attachment left linked. A +successful send is audited as `payroll.payslip_mail`. `dry_run` checks the +recipient and the configuration, renders the PDF and returns the recipient +and file name without storing or sending anything. + The settings `payroll_salary_account` (`7210`, the default for new employees), `payroll_tax_account` (`2710`), `payroll_avgift_account` (`7510`), `payroll_avgift_liability` (`2731`), @@ -832,8 +858,8 @@ Args: `name:type(values)[!][=default]`, `!` = required. | `invoice.send` | bookkeeper | yes | yes | yes | `id:int!`, `to:string` | | `employee.list` | viewer | yes | no | no | `active_only:bool` | | `employee.get` | viewer | yes | no | no | `id:int!` | -| `employee.create` | bookkeeper | yes | yes | yes | `name:string!`, `personal_no:string!`, `address:string`, `postal_code:string`, `city:string`, `bank_account:string`, `salary_account:string`, `monthly_salary_ore:int=0`, `tax_table:int=30`, `tax_column:int=1` | -| `employee.update` | bookkeeper | yes | yes | yes | `id:int!`, `name:string`, `personal_no:string`, `address:string`, `postal_code:string`, `city:string`, `bank_account:string`, `salary_account:string`, `monthly_salary_ore:int`, `tax_table:int`, `tax_column:int`, `active:bool` | +| `employee.create` | bookkeeper | yes | yes | yes | `name:string!`, `personal_no:string!`, `address:string`, `postal_code:string`, `city:string`, `bank_account:string`, `email:string`, `salary_account:string`, `monthly_salary_ore:int=0`, `tax_table:int=30`, `tax_column:int=1` | +| `employee.update` | bookkeeper | yes | yes | yes | `id:int!`, `name:string`, `personal_no:string`, `address:string`, `postal_code:string`, `city:string`, `bank_account:string`, `email:string`, `salary_account:string`, `monthly_salary_ore:int`, `tax_table:int`, `tax_column:int`, `active:bool` | | `employee.archive` | bookkeeper | yes | yes | yes | `id:int!`, `active:bool!` | | `payroll.tax_tables_fetch` | owner | yes | yes | yes | `year:int` | | `payroll.tax_tables_import` | owner | yes | yes | yes | `year:int!`, `content_base64:string!` | @@ -842,6 +868,8 @@ Args: `name:type(values)[!][=default]`, `!` = required. | `payroll.run_post` | bookkeeper | yes | yes | yes | `period:string!`, `pay_date:date!` | | `payroll.run_list` | viewer | yes | no | no | `limit:int=100` | | `payroll.run_get` | viewer | yes | no | no | `id:int!` | +| `payroll.payslip` | viewer | yes | no | no | `run_id:int!`, `employee_id:int` | +| `payroll.payslip_mail` | bookkeeper | yes | yes | yes | `run_id:int!`, `employee_id:int`, `to:string` | | `payroll.agi` | owner | yes | no | no | `period:string!` | | `payroll.pay_tax` | bookkeeper | yes | yes | yes | `run_id:int!`, `date:date` | | `payroll.settings_get` | viewer | yes | no | no | — | |
