cetustek 0.2.0 → 0.3.0
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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/CHANGELOG.md +22 -0
- data/README.md +73 -3
- data/lib/cetustek/models/invoice_data.rb +40 -8
- data/lib/cetustek/services/invoice_xml_builder.rb +31 -59
- data/lib/cetustek/version.rb +1 -1
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc27cec134bbdcde72328147677ace14f302f2423128209d8ec0ddd190ceeca7
|
|
4
|
+
data.tar.gz: cfb7115017ef61ba4c928459f1d8d8815bf5a1fd616cc050a8be012a942aac3e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a350443ea05a452ec354e1f9d7a5cb624cd04087baecef9f756c715350497bdc6d9b0fbedc41ee2087d711aba66b72ecfbb7c207d239d8b8278266335a3eae4
|
|
7
|
+
data.tar.gz: 5ac422d52d5d11b0bf71d97186ab9bd257f667ec135c942f87df934f66cb4f7653a40cda4794d62171aa72d905f98d60fe46e416409399e07abbfe679a063130
|
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--require spec_helper
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-06-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Configurable tax handling per spec V4.16: `TaxType`, `TaxRate`, and `InvoiceType`
|
|
12
|
+
on `InvoiceData`, with a `Cetustek::TaxType` constants module
|
|
13
|
+
- Zero-rate invoice support (`TaxType` 2/5)
|
|
14
|
+
- Mixed-tax invoice support (`TaxType` 9): per-item `tax_type` emits the required
|
|
15
|
+
`DType` (`TZ`/`TN`/blank) on every detail line
|
|
16
|
+
- `carrier_id2` attribute on `InvoiceData` (previously read by the XML builder but
|
|
17
|
+
never settable, which raised `NoMethodError` when issuing an invoice)
|
|
18
|
+
- Test suite for the XML builder and data models
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- XML builder now HTML-escapes every interpolated field (previously only buyer name
|
|
22
|
+
and product name), preventing malformed XML / injection from special characters
|
|
23
|
+
|
|
24
|
+
### Removed
|
|
25
|
+
- **Breaking:** application-specific auto line items and their `InvoiceData` fields
|
|
26
|
+
(`total_discount`, `coupon_discount`, `delivery_fee`, `handling_fee`). These were
|
|
27
|
+
not part of the Cetustek API. Model discounts/fees as ordinary `InvoiceItem`s
|
|
28
|
+
(use a negative `unit_price` for a discount)
|
|
29
|
+
|
|
8
30
|
## [0.2.0] - 2025-01-22
|
|
9
31
|
|
|
10
32
|
### Added
|
data/README.md
CHANGED
|
@@ -44,7 +44,7 @@ end
|
|
|
44
44
|
|
|
45
45
|
## Usage
|
|
46
46
|
|
|
47
|
-
###
|
|
47
|
+
### Issue an Invoice
|
|
48
48
|
|
|
49
49
|
```ruby
|
|
50
50
|
invoice = YourInvoiceModel.find(invoice_id)
|
|
@@ -54,7 +54,9 @@ invoice_data = Cetustek::Models::InvoiceData.new(
|
|
|
54
54
|
buyer_identifier: invoice.receipt,
|
|
55
55
|
buyer_name: invoice.name,
|
|
56
56
|
buyer_email: invoice.email,
|
|
57
|
-
|
|
57
|
+
donate_mark: 0,
|
|
58
|
+
payment_type: 2,
|
|
59
|
+
items: invoice.items.map { |item|
|
|
58
60
|
Cetustek::Models::InvoiceItem.new(
|
|
59
61
|
code: item.sku,
|
|
60
62
|
name: item.name,
|
|
@@ -65,6 +67,73 @@ invoice_data = Cetustek::Models::InvoiceData.new(
|
|
|
65
67
|
)
|
|
66
68
|
|
|
67
69
|
result = Cetustek::CreateInvoice.new(invoice_data).execute
|
|
70
|
+
# => { number: "GT68514542", random_number: "9654" }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Tax types (稅別)
|
|
74
|
+
|
|
75
|
+
`InvoiceData` defaults to taxable (`TaxType` 1) with a tax rate of `0.05` and a
|
|
76
|
+
general invoice type of `07`. Use `Cetustek::TaxType` to switch modes:
|
|
77
|
+
|
|
78
|
+
| Constant | Code | Meaning |
|
|
79
|
+
|----------|------|---------|
|
|
80
|
+
| `TAXABLE` | 1 | 應稅 |
|
|
81
|
+
| `ZERO_RATE` | 2 | 零稅率(非經海關出口) |
|
|
82
|
+
| `TAX_FREE` | 3 | 免稅 |
|
|
83
|
+
| `SPECIAL` | 4 | 應稅(特種稅率) — set `tax_rate`, use `invoice_type: '08'` |
|
|
84
|
+
| `ZERO_RATE_CUSTOMS` | 5 | 零稅率(經海關出口) |
|
|
85
|
+
| `MIXED` | 9 | 混合(應稅/零稅率/免稅,限收銀機類型發票) |
|
|
86
|
+
|
|
87
|
+
#### Zero-rate invoice (零稅率)
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
Cetustek::Models::InvoiceData.new(
|
|
91
|
+
# ...buyer fields, items...
|
|
92
|
+
tax_type: Cetustek::TaxType::ZERO_RATE,
|
|
93
|
+
tax_rate: 0
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### Mixed-tax invoice (混稅, cash-register invoices only)
|
|
98
|
+
|
|
99
|
+
For `TaxType` 9 each line item must declare its own tax category via `tax_type`.
|
|
100
|
+
Accepts the symbols `:taxable` (default), `:zero_rate`, `:tax_free`, or the raw
|
|
101
|
+
`DType` codes (`''`, `'TZ'`, `'TN'`):
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
Cetustek::Models::InvoiceData.new(
|
|
105
|
+
# ...buyer fields...
|
|
106
|
+
tax_type: Cetustek::TaxType::MIXED,
|
|
107
|
+
items: [
|
|
108
|
+
Cetustek::Models::InvoiceItem.new(code: 'A', name: '應稅品', quantity: 1, unit_price: 100),
|
|
109
|
+
Cetustek::Models::InvoiceItem.new(code: 'B', name: '零稅率品', quantity: 1, unit_price: 100, tax_type: :zero_rate),
|
|
110
|
+
Cetustek::Models::InvoiceItem.new(code: 'C', name: '免稅品', quantity: 1, unit_price: 100, tax_type: :tax_free)
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Discounts and fees
|
|
116
|
+
|
|
117
|
+
The gem is a faithful wrapper of the API's invoice detail format, so it has no
|
|
118
|
+
built-in discount/coupon/delivery/handling concepts. Model them as ordinary line
|
|
119
|
+
items — use a negative `unit_price` for a discount:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
Cetustek::Models::InvoiceItem.new(code: 'DISCOUNT', name: '折抵', quantity: 1, unit_price: -30)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Cancel an Invoice
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# `invoice` responds to #number and #created_at; on success (return code "C0")
|
|
129
|
+
# it is updated with canceled: true.
|
|
130
|
+
Cetustek::CancelInvoice.new(invoice).execute
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Query an Invoice by Order ID
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
response = Cetustek::QueryInvoiceByOrderId.query(order_id)
|
|
68
137
|
```
|
|
69
138
|
|
|
70
139
|
## Development
|
|
@@ -72,10 +141,11 @@ result = Cetustek::CreateInvoice.new(invoice_data).execute
|
|
|
72
141
|
1. Clone this repository
|
|
73
142
|
2. Run `bin/setup` to install dependencies
|
|
74
143
|
3. Run `bin/console` for an interactive prompt to experiment
|
|
144
|
+
4. Run `bundle exec rspec` to run the test suite
|
|
75
145
|
|
|
76
146
|
## Requirements
|
|
77
147
|
|
|
78
|
-
- Ruby >=
|
|
148
|
+
- Ruby >= 3.0.0
|
|
79
149
|
- `ox` gem for XML processing
|
|
80
150
|
- `savon` gem for SOAP services
|
|
81
151
|
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Cetustek
|
|
4
|
+
# TaxType (稅別) codes accepted by CreateInvoiceV3 (spec V4.16, Table 1).
|
|
5
|
+
module TaxType
|
|
6
|
+
TAXABLE = 1 # 應稅
|
|
7
|
+
ZERO_RATE = 2 # 零稅率(非經海關出口)
|
|
8
|
+
TAX_FREE = 3 # 免稅
|
|
9
|
+
SPECIAL = 4 # 應稅(特種稅率) — requires TaxRate
|
|
10
|
+
ZERO_RATE_CUSTOMS = 5 # 零稅率(經海關出口)
|
|
11
|
+
MIXED = 9 # 混合(應稅、零稅率與免稅,限收銀機類型發票)
|
|
12
|
+
end
|
|
13
|
+
|
|
4
14
|
module Models
|
|
5
15
|
class InvoiceData
|
|
16
|
+
DEFAULT_TAX_RATE = 0.05
|
|
17
|
+
DEFAULT_INVOICE_TYPE = '07' # 07: 一般稅額, 08: 特種稅額
|
|
18
|
+
|
|
6
19
|
attr_reader :order_id, :order_date, :buyer_identifier, :buyer_name,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
20
|
+
:buyer_email, :donate_mark, :carrier_type, :carrier_id,
|
|
21
|
+
:carrier_id2, :npo_ban, :items, :payment_type,
|
|
22
|
+
:tax_type, :tax_rate, :invoice_type
|
|
10
23
|
|
|
11
24
|
def initialize(attributes = {})
|
|
12
25
|
@order_id = attributes[:order_id]
|
|
@@ -17,24 +30,43 @@ module Cetustek
|
|
|
17
30
|
@donate_mark = attributes[:donate_mark]
|
|
18
31
|
@carrier_type = attributes[:carrier_type]
|
|
19
32
|
@carrier_id = attributes[:carrier_id]
|
|
33
|
+
@carrier_id2 = attributes[:carrier_id2]
|
|
20
34
|
@npo_ban = attributes[:npo_ban]
|
|
21
35
|
@items = attributes[:items] || []
|
|
22
36
|
@payment_type = attributes[:payment_type]
|
|
23
|
-
@
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
|
|
37
|
+
@tax_type = attributes[:tax_type] || TaxType::TAXABLE
|
|
38
|
+
@tax_rate = attributes.fetch(:tax_rate, DEFAULT_TAX_RATE)
|
|
39
|
+
@invoice_type = attributes[:invoice_type] || DEFAULT_INVOICE_TYPE
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 混合稅率發票 (限收銀機):每筆明細需標註 DType。
|
|
43
|
+
def mixed_tax?
|
|
44
|
+
@tax_type.to_i == TaxType::MIXED
|
|
27
45
|
end
|
|
28
46
|
end
|
|
29
47
|
|
|
30
48
|
class InvoiceItem
|
|
31
|
-
|
|
49
|
+
# Per-item 稅別註記 (DType) used for mixed-tax invoices (TaxType == 9).
|
|
50
|
+
DTYPE_MAP = {
|
|
51
|
+
taxable: '', # 應稅商品 -> 空白
|
|
52
|
+
zero_rate: 'TZ', # 零稅率商品
|
|
53
|
+
tax_free: 'TN' # 免稅商品
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
56
|
+
attr_reader :code, :name, :quantity, :unit_price, :tax_type
|
|
32
57
|
|
|
33
58
|
def initialize(attributes = {})
|
|
34
59
|
@code = attributes[:code]
|
|
35
60
|
@name = attributes[:name]
|
|
36
61
|
@quantity = attributes[:quantity]
|
|
37
62
|
@unit_price = attributes[:unit_price]
|
|
63
|
+
@tax_type = attributes[:tax_type] || :taxable
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns the DType code: '', 'TZ' or 'TN'.
|
|
67
|
+
# Accepts the friendly symbols above or a raw code string.
|
|
68
|
+
def d_type
|
|
69
|
+
DTYPE_MAP.fetch(@tax_type) { @tax_type.to_s }
|
|
38
70
|
end
|
|
39
71
|
end
|
|
40
72
|
end
|
|
@@ -27,6 +27,13 @@ module Cetustek
|
|
|
27
27
|
instruct
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# Builds a raw XML element with the value HTML-escaped, so that special
|
|
31
|
+
# characters (&, <, >, ", ') in any dynamic field cannot break the XML
|
|
32
|
+
# or be used for injection.
|
|
33
|
+
def raw_tag(name, value)
|
|
34
|
+
Ox::Raw.new("<#{name}>#{CGI.escapeHTML(value.to_s)}</#{name}>")
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
def create_invoice_element
|
|
31
38
|
invoice = Ox::Element.new('Invoice')
|
|
32
39
|
invoice[:XSDVersion] = '2.8'
|
|
@@ -40,25 +47,26 @@ module Cetustek
|
|
|
40
47
|
end
|
|
41
48
|
|
|
42
49
|
def add_basic_info(invoice)
|
|
43
|
-
invoice <<
|
|
44
|
-
invoice <<
|
|
50
|
+
invoice << raw_tag('OrderId', @data.order_id)
|
|
51
|
+
invoice << raw_tag('OrderDate', @data.order_date.strftime('%Y/%m/%d'))
|
|
45
52
|
end
|
|
46
53
|
|
|
47
54
|
def add_buyer_info(invoice)
|
|
48
|
-
invoice <<
|
|
49
|
-
invoice <<
|
|
50
|
-
invoice <<
|
|
55
|
+
invoice << raw_tag('BuyerIdentifier', @data.buyer_identifier)
|
|
56
|
+
invoice << raw_tag('BuyerName', @data.buyer_name)
|
|
57
|
+
invoice << raw_tag('BuyerEmailAddress', @data.buyer_email)
|
|
51
58
|
end
|
|
52
59
|
|
|
53
60
|
def add_invoice_type_info(invoice)
|
|
54
|
-
invoice <<
|
|
55
|
-
invoice <<
|
|
56
|
-
invoice <<
|
|
57
|
-
invoice <<
|
|
58
|
-
invoice <<
|
|
59
|
-
invoice <<
|
|
60
|
-
invoice <<
|
|
61
|
-
invoice <<
|
|
61
|
+
invoice << raw_tag('DonateMark', @data.donate_mark)
|
|
62
|
+
invoice << raw_tag('InvoiceType', @data.invoice_type)
|
|
63
|
+
invoice << raw_tag('CarrierType', @data.carrier_type)
|
|
64
|
+
invoice << raw_tag('CarrierId1', @data.carrier_id)
|
|
65
|
+
invoice << raw_tag('CarrierId2', @data.carrier_id2)
|
|
66
|
+
invoice << raw_tag('NPOBAN', @data.npo_ban)
|
|
67
|
+
invoice << raw_tag('TaxType', @data.tax_type)
|
|
68
|
+
invoice << raw_tag('TaxRate', @data.tax_rate)
|
|
69
|
+
invoice << raw_tag('PayWay', @data.payment_type)
|
|
62
70
|
end
|
|
63
71
|
|
|
64
72
|
def add_details(invoice)
|
|
@@ -68,60 +76,24 @@ module Cetustek
|
|
|
68
76
|
@data.items.each do |item|
|
|
69
77
|
details << create_product_item(item)
|
|
70
78
|
end
|
|
71
|
-
|
|
72
|
-
add_additional_items(details)
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
def create_product_item(item)
|
|
76
82
|
product = Ox::Element.new('ProductItem')
|
|
77
|
-
product <<
|
|
78
|
-
product <<
|
|
79
|
-
product <<
|
|
80
|
-
product <<
|
|
83
|
+
product << raw_tag('ProductionCode', item.code)
|
|
84
|
+
product << raw_tag('Description', item.name)
|
|
85
|
+
product << raw_tag('Quantity', item.quantity)
|
|
86
|
+
product << raw_tag('UnitPrice', item.unit_price)
|
|
87
|
+
add_dtype(product, item.d_type)
|
|
81
88
|
product
|
|
82
89
|
end
|
|
83
90
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
add_handling_fee_item(details) if @data.handling_fee.positive?
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def add_discount_item(details)
|
|
92
|
-
discount = Ox::Element.new('ProductItem')
|
|
93
|
-
discount << Ox::Raw.new('<ProductionCode>DISCOUNT</ProductionCode>')
|
|
94
|
-
discount << Ox::Raw.new('<Description>折抵金額</Description>')
|
|
95
|
-
discount << Ox::Raw.new('<Quantity>1</Quantity>')
|
|
96
|
-
discount << Ox::Raw.new("<UnitPrice>#{@data.total_discount * -1}</UnitPrice>")
|
|
97
|
-
details << discount
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def add_coupon_item(details)
|
|
101
|
-
coupon = Ox::Element.new('ProductItem')
|
|
102
|
-
coupon << Ox::Raw.new('<ProductionCode>COUPON</ProductionCode>')
|
|
103
|
-
coupon << Ox::Raw.new('<Description>分享折讓</Description>')
|
|
104
|
-
coupon << Ox::Raw.new('<Quantity>1</Quantity>')
|
|
105
|
-
coupon << Ox::Raw.new("<UnitPrice>#{@data.coupon_discount * -1}</UnitPrice>")
|
|
106
|
-
details << coupon
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def add_delivery_fee_item(details)
|
|
110
|
-
delivery = Ox::Element.new('ProductItem')
|
|
111
|
-
delivery << Ox::Raw.new('<ProductionCode>DELIVERY_FEE</ProductionCode>')
|
|
112
|
-
delivery << Ox::Raw.new('<Description>運費</Description>')
|
|
113
|
-
delivery << Ox::Raw.new('<Quantity>1</Quantity>')
|
|
114
|
-
delivery << Ox::Raw.new("<UnitPrice>#{@data.delivery_fee}</UnitPrice>")
|
|
115
|
-
details << delivery
|
|
116
|
-
end
|
|
91
|
+
# DType (稅別註記) is required on every detail line only for mixed-tax
|
|
92
|
+
# invoices (TaxType == 9).
|
|
93
|
+
def add_dtype(product, value)
|
|
94
|
+
return unless @data.mixed_tax?
|
|
117
95
|
|
|
118
|
-
|
|
119
|
-
handling = Ox::Element.new('ProductItem')
|
|
120
|
-
handling << Ox::Raw.new('<ProductionCode>HANDLING_FEE</ProductionCode>')
|
|
121
|
-
handling << Ox::Raw.new('<Description>手續費</Description>')
|
|
122
|
-
handling << Ox::Raw.new('<Quantity>1</Quantity>')
|
|
123
|
-
handling << Ox::Raw.new("<UnitPrice>#{@data.handling_fee}</UnitPrice>")
|
|
124
|
-
details << handling
|
|
96
|
+
product << raw_tag('DType', value)
|
|
125
97
|
end
|
|
126
98
|
end
|
|
127
99
|
end
|
data/lib/cetustek/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cetustek
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Zac
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ox
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '13.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.12'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.12'
|
|
54
68
|
description: Cetustek is a Ruby gem designed for handling electronic invoice operations,
|
|
55
69
|
including invoice cancellation. It communicates with the e-invoice system through
|
|
56
70
|
SOAP Web Services.
|
|
@@ -60,6 +74,7 @@ executables: []
|
|
|
60
74
|
extensions: []
|
|
61
75
|
extra_rdoc_files: []
|
|
62
76
|
files:
|
|
77
|
+
- ".rspec"
|
|
63
78
|
- CHANGELOG.md
|
|
64
79
|
- README.md
|
|
65
80
|
- Rakefile
|
|
@@ -95,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
95
110
|
- !ruby/object:Gem::Version
|
|
96
111
|
version: '0'
|
|
97
112
|
requirements: []
|
|
98
|
-
rubygems_version:
|
|
113
|
+
rubygems_version: 4.0.10
|
|
99
114
|
specification_version: 4
|
|
100
115
|
summary: A Ruby gem for handling electronic invoice operations
|
|
101
116
|
test_files: []
|