aboutsummaryrefslogtreecommitdiffstats
path: root/print.go
blob: 8532fff5f8e629c44b97b626154a37c1651b9023 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
package lulu

import (
	"encoding/json"
	"fmt"
	"time"
)

const (
	MinProductionDelay = 60 * time.Minute
	MaxProductionDelay = 48 * time.Hour
)

type printReq struct {
	Contact             EmailAddress    `json:"contact_email"`
	ExternalId          string          `json:"external_id"`
	LineItems           []Printable     `json:"line_items"`
	ProductionDelayMins uint            `json:"production_delay"`
	ShipAddr            ShippingAddress `json:"shipping_address"`
	ShipOpt             ShippingLevel   `json:"shipping_level"`
}

// PrintableId uniquely identifies a book to be printed: a "printable". A
// printable consists of a cover and an interior file as well as a
// PodPkgId which specifies the manufacturing options.
type PrintableId string

// Printable is a book to be printed.
type Printable struct {
	// Arbitrary string to identify and connect a print job to your
	// systems. Set it to an order number, a purchase order or
	// whatever else works for your particular use case.
	ExternalId string `json:"external_id"`

	// Interior and cover files have to be specified with a URL from
	// which Lulu can download the files. Using encoded basic
	// authentication in the URL is ok. All files processed by Lulu
	// will be validated and normalized before sending them to
	// production. If problems with the file occur, the PrintJob will
	// be rejected or cancelled and an error message will be
	// displayed.
	CoverUrl    string `json:"cover"`
	InteriorUrl string `json:"interior"`

	// Manufacturing options.
	Mfg PkgId `json:"pod_package_id"`

	// The number of copies to print.
	Quantity uint `json:"quantity"`

	// The title of the book. Must not exceed 255 bytes.
	Title string `json:"title"`
}

type getPrintJobsResp struct {
	Count   uint       `json:"count"`
	Next    string     `json:"next"`
	Prev    string     `json:"previous"`
	Results []PrintJob `json:"results"`
}

type PrintJob struct {
	// An email address for questions regarding the print
	// job—normally, you want to use the email address of a
	// developer or shop owner, not the end customer.
	Contact EmailAddress `json:"contact_email"`

	Cost PrintJobCost `json:"costs"`

	Created  time.Time `json:"date_created"`
	Modified time.Time `json:"date_modified"`

	EstimatedShippingDates EstimatedShippingDates `json:"estimated_shipping_dates"`

	// Arbitrary string to identify and connect a print job to your
	// systems. Set it to an order number, a purchase order or
	// whatever else works for your particular use case.
	ExternalId string `json:"external_id"`

	Id uint64 `json:"id"`

	LineItems []LineItem `json:"line_items"`

	OrderId string `json:"order_id"`

	// Delay before a newly created Print-Job is sent to production.
	// Minimum is 60 minutes, maximum is 2880 minutes (=48 hours). As
	// most cancellation requests occur right after an order has been
	// placed, it makes sense to wait for some time before sending an
	// order to production. Once production has started, orders
	// cannot be canceled anymore.
	ProductionDelay time.Duration

	// Target timestamp of when this job will move into production.
	ProductionDue time.Time `json:"production_due_time"`

	AddressValidation ShippingAddressValidation `json:"shipping_address"`

	// The shipping level that this Print-Job is shipped with.
	ShipOpt ShippingLevel `json:"shipping_level"`

	// ISO 3166-1 alpha-2 country code of the tax country determined for this job
	TaxCountry string `json:"tax_country"`

	Status PrintJobStatus `json:"status"`
}

func (pj *PrintJob) UnmarshalJSON(data []byte) error {
	type alias PrintJob // prevent infinite recursion
	if err := json.Unmarshal(data, (*alias)(pj)); err != nil {
		return err
	}
	var extra struct {
		ProductionDelay uint `json:"production_delay"`
	}
	if err := json.Unmarshal(data, &extra); err != nil {
		return fmt.Errorf("error unmarshaling %T.ProductionDelay: %w", *pj, err)
	}
	pj.ProductionDelay = time.Minute * time.Duration(extra.ProductionDelay)
	return nil
}

// LineItem represents a book that should be printed: a "printable" for short.
type LineItem struct {
	// Arbitrary string to identify and connect a print job to your
	// systems. Set it to an order number, a purchase order or
	// whatever else works for your particular use case.
	ExternalId string `json:"external_id"`

	Id uint64 `json:"id"`

	NPages uint `json:"page_count"`

	// Manufacturing options.
	Mfg PkgId `json:"pod_package_id"`

	// Id of the printable. It can be used instead of PrintableNormalization.
	PrintableId PrintableId `json:"printable_id"`

	// Normalization process of the cover and interior source files.
	PrintableNormalization PrintableNormalization `json:"printable_normalization"`

	// Quantity of printed books.
	Quantity uint `json:"quantity"`

	Status LineItemStatus `json:"status"`

	// Title of the line item. Should be on the cover.
	Title string `json:"title"`

	// Tracking id for this line item's shipment.
	TrackingId string `json:"tracking_id"`

	// A list of tracking urls for this line item's shipment.
	TrackingUrls []string `json:"tracking_urls"`

	// Name of the carrier handling the shipment.
	Carrier string `json:"carrier_name"`
}

type EstimatedShippingDates struct {
	ArrivalMax  Date `json:"arrival_max"`
	ArrivalMin  Date `json:"arrival_min"`
	DispatchMax Date `json:"dispatch_max"`
	DispatchMin Date `json:"dispatch_min"`
}

// PrintableNormalization represents the normalization processes of the
// interior and cover source files.
type PrintableNormalization struct {
	Cover    NormalizationJob `json:"cover"`
	Interior NormalizationJob `json:"interior"`
}

// NormalizationJob represents the normalization process of an interior
// or cover source file.
type NormalizationJob struct {
	JobId          uint           `json:"job_id"`
	NormalizedFile NormalizedFile `json:"normalized_file"`
	SrcMd5Sum      string         `json:"source_md5_sum"` // md5 hash of the source file.
	SrcUrl         string         `json:"source_url"`     // URL of the source file.
}

// NormalizedFile represents a file on the server that was created by
// normalizing a cover or interior source file.
type NormalizedFile struct {
	Id   uint64 `json:"file_id"`
	Name string `json:"filename"`
}

type LineItemStatus struct {
	Messages LineItemStatusMessages `json:"messages"`
	Status   ItemStatus             `json:"name"`
}

type LineItemStatusMessages struct {
	Delay                  time.Duration          // Expected delay due to the error, if present.
	Error                  string                 `json:"error"`
	Info                   string                 `json:"info"`
	PrintableNormalization PrintableNormalization `json:"printable_normalization"`
	Timestamp              time.Time              `json:"timestamp"` // Timestamp of the last status change.
	TrackingUrls           []string
	TrackingId             string `json:"tracking_id"`  // Tracking ID for this line item's shipment.
	Carrier                string `json:"carrier_name"` // Name of the carrier handling the shipment.
}

func (msgs *LineItemStatusMessages) UnmarshalJSON(data []byte) error {
	type alias LineItemStatusMessages // prevent infinite recursion
	if err := json.Unmarshal(data, (*alias)(msgs)); err != nil {
		return err
	}

	var extra struct {
		Delay        string          `json:"delay"`
		TrackingUrls json.RawMessage `json:"tracking_urls"`
	}
	if err := json.Unmarshal(data, &extra); err != nil {
		return err
	}

	var err error
	if len(extra.Delay) > 0 {
		msgs.Delay, err = time.ParseDuration(fmt.Sprintf("%sh", extra.Delay))
		if err != nil {
			return fmt.Errorf("error unmarshaling %T.Delay: %w", *msgs, err)
		}
	}

	if len(extra.TrackingUrls) > 0 {
		msgs.TrackingUrls, err = unmarshalSliceOrVal[string](extra.TrackingUrls)
		if err != nil {
			return fmt.Errorf("error unmarshaling %T.TrackingUrls: %w", *msgs, err)
		}
	}

	return nil
}

type PrintJobStatus struct {
	Changed time.Time   `json:"changed"` // time of the last status change
	Msg     string      `json:"message"`
	Status  OrderStatus `json:"name"`
}