github.com/readium/readium-lcp-server@v0.0.0-20240509124024-799e77a0bbd6/frontend/webpurchase/webpurchase.go (about)

     1  // Copyright 2020 Readium Foundation. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license
     3  // that can be found in the LICENSE file exposed on Github (readium) in the project repository.
     4  
     5  package webpurchase
     6  
     7  import (
     8  	"bytes"
     9  	"database/sql"
    10  	"encoding/hex"
    11  	"encoding/json"
    12  	"errors"
    13  	"log"
    14  	"net/http"
    15  	"time"
    16  
    17  	"github.com/readium/readium-lcp-server/api"
    18  	"github.com/readium/readium-lcp-server/config"
    19  	"github.com/readium/readium-lcp-server/dbutils"
    20  	"github.com/readium/readium-lcp-server/frontend/webpublication"
    21  	"github.com/readium/readium-lcp-server/frontend/webuser"
    22  	"github.com/readium/readium-lcp-server/license"
    23  	licensestatuses "github.com/readium/readium-lcp-server/license_statuses"
    24  	uuid "github.com/satori/go.uuid"
    25  )
    26  
    27  // ErrNotFound Error is thrown when a purchase is not found
    28  var ErrNotFound = errors.New("purchase not found")
    29  
    30  // ErrNoChange is thrown when an update action does not change any rows (not found)
    31  var ErrNoChange = errors.New("no lines were updated")
    32  
    33  // WebPurchase defines possible interactions with the db
    34  type WebPurchase interface {
    35  	Get(id int64) (Purchase, error)
    36  	GenerateOrGetLicense(purchase Purchase) (license.License, error)
    37  	GetPartialLicense(purchase Purchase) (license.License, error)
    38  	GetLicenseStatusDocument(purchase Purchase) (licensestatuses.LicenseStatus, error)
    39  	GetByLicenseID(licenseID string) (Purchase, error)
    40  	List(page int, pageNum int) func() (Purchase, error)
    41  	ListByUser(userID int64, page int, pageNum int) func() (Purchase, error)
    42  	Add(p Purchase) error
    43  	Update(p Purchase) error
    44  }
    45  
    46  // Purchase status
    47  const (
    48  	StatusToBeRenewed  string = "to-be-renewed"
    49  	StatusToBeReturned string = "to-be-returned"
    50  	StatusError        string = "error"
    51  	StatusOk           string = "ok"
    52  )
    53  
    54  // Enumeration of PurchaseType
    55  const (
    56  	BUY  string = "BUY"
    57  	LOAN string = "LOAN"
    58  )
    59  
    60  // Purchase struct defines a user in json and database
    61  // PurchaseType: BUY or LOAN
    62  type Purchase struct {
    63  	ID              int64                      `json:"id,omitempty"`
    64  	UUID            string                     `json:"uuid"`
    65  	Publication     webpublication.Publication `json:"publication"`
    66  	User            webuser.User               `json:"user"`
    67  	LicenseUUID     *string                    `json:"licenseUuid,omitempty"`
    68  	Type            string                     `json:"type"`
    69  	TransactionDate time.Time                  `json:"transactionDate,omitempty"`
    70  	StartDate       *time.Time                 `json:"startDate,omitempty"`
    71  	EndDate         *time.Time                 `json:"endDate,omitempty"`
    72  	Status          string                     `json:"status"`
    73  	MaxEndDate      *time.Time                 `json:"maxEndDate,omitempty"`
    74  }
    75  
    76  type PurchaseManager struct {
    77  	db               *sql.DB
    78  	dbGetByID        *sql.Stmt
    79  	dbGetByLicenseID *sql.Stmt
    80  	dbList           *sql.Stmt
    81  	dbListByUser     *sql.Stmt
    82  }
    83  
    84  func convertRecordsToPurchases(rows *sql.Rows) func() (Purchase, error) {
    85  
    86  	return func() (Purchase, error) {
    87  		var err error
    88  		var purchase Purchase
    89  		if rows == nil {
    90  			return purchase, ErrNotFound
    91  		}
    92  		if rows.Next() {
    93  			purchase, err = convertRecordToPurchase(rows)
    94  			if err != nil {
    95  				return purchase, err
    96  			}
    97  		} else {
    98  			rows.Close()
    99  			err = ErrNotFound
   100  		}
   101  		return purchase, err
   102  	}
   103  }
   104  
   105  func convertRecordToPurchase(rows *sql.Rows) (Purchase, error) {
   106  	purchase := Purchase{}
   107  	user := webuser.User{}
   108  	pub := webpublication.Publication{}
   109  
   110  	err := rows.Scan(
   111  		&purchase.ID,
   112  		&purchase.UUID,
   113  		&purchase.Type,
   114  		&purchase.TransactionDate,
   115  		&purchase.LicenseUUID,
   116  		&purchase.StartDate,
   117  		&purchase.EndDate,
   118  		&purchase.Status,
   119  		&user.ID,
   120  		&user.UUID,
   121  		&user.Name,
   122  		&user.Email,
   123  		&user.Password,
   124  		&user.Hint,
   125  		&pub.ID,
   126  		&pub.UUID,
   127  		&pub.Title,
   128  		&pub.Status)
   129  
   130  	if err != nil {
   131  		return Purchase{}, err
   132  	}
   133  
   134  	// Load relations
   135  	purchase.User = user
   136  	purchase.Publication = pub
   137  	return purchase, nil
   138  }
   139  
   140  // Get a purchase using its id
   141  func (pManager PurchaseManager) Get(id int64) (Purchase, error) {
   142  
   143  	// note: does not use QueryRow here, as converRecordToPurchase used in converRecordsToPurchase.
   144  	rows, err := pManager.dbGetByID.Query(id)
   145  	if err != nil {
   146  		return Purchase{}, err
   147  	}
   148  	defer rows.Close()
   149  
   150  	if rows.Next() {
   151  		purchase, err := convertRecordToPurchase(rows)
   152  		if err != nil {
   153  			return Purchase{}, err
   154  		}
   155  
   156  		if purchase.LicenseUUID != nil {
   157  			// Query LSD Server to retrieve max end date (PotentialRights.End)
   158  			// FIXME: calling the lsd server at this point is too heavy: the max end date should be in the db.
   159  			statusDocument, err := pManager.GetLicenseStatusDocument(purchase)
   160  
   161  			if err != nil {
   162  				return Purchase{}, err
   163  			}
   164  
   165  			if statusDocument.PotentialRights != nil && statusDocument.PotentialRights.End != nil && !(*statusDocument.PotentialRights.End).IsZero() {
   166  				purchase.MaxEndDate = statusDocument.PotentialRights.End
   167  			}
   168  		}
   169  		return purchase, nil
   170  	}
   171  	return Purchase{}, ErrNotFound
   172  }
   173  
   174  // GetByLicenseID gets a purchase by the associated license id
   175  func (pManager PurchaseManager) GetByLicenseID(licenseID string) (Purchase, error) {
   176  
   177  	// note: does not use QueryRow here, as convertRecordToPurchase used in converRecordsToPurchase.
   178  	rows, err := pManager.dbGetByLicenseID.Query(licenseID)
   179  	if err != nil {
   180  		return Purchase{}, err
   181  	}
   182  	defer rows.Close()
   183  
   184  	if rows.Next() {
   185  		return convertRecordToPurchase(rows)
   186  		// FIXME: difference with Get(), we don't retrieve the potential end date from the LSD server
   187  	}
   188  	return Purchase{}, ErrNotFound
   189  }
   190  
   191  // GenerateOrGetLicense generates a new license associated with a purchase,
   192  // or gets an existing license,
   193  // depending on the value of the license id in the purchase.
   194  func (pManager PurchaseManager) GenerateOrGetLicense(purchase Purchase) (license.License, error) {
   195  	// create a partial license
   196  	partialLicense := license.License{}
   197  
   198  	// set the mandatory provider URI
   199  	if config.Config.FrontendServer.ProviderUri == "" {
   200  		return license.License{}, errors.New("mandatory provider URI missing in the configuration")
   201  	}
   202  	partialLicense.Provider = config.Config.FrontendServer.ProviderUri
   203  
   204  	// get user info from the purchase info
   205  	encryptedAttrs := []string{"email", "name"}
   206  	partialLicense.User.Email = purchase.User.Email
   207  	partialLicense.User.Name = purchase.User.Name
   208  	partialLicense.User.ID = purchase.User.UUID
   209  	partialLicense.User.Encrypted = encryptedAttrs
   210  
   211  	// get the hashed passphrase from the purchase
   212  	userKeyValue, err := hex.DecodeString(purchase.User.Password)
   213  
   214  	if err != nil {
   215  		return license.License{}, err
   216  	}
   217  
   218  	userKey := license.UserKey{}
   219  	userKey.Algorithm = "http://www.w3.org/2001/04/xmlenc#sha256"
   220  	userKey.Hint = purchase.User.Hint
   221  	userKey.Value = userKeyValue
   222  	partialLicense.Encryption.UserKey = userKey
   223  
   224  	// In case of a creation of license, add the user rights
   225  	var copy, print int32
   226  	if purchase.LicenseUUID == nil {
   227  		// in case of undefined conf values for copy and print rights,
   228  		// these rights will be set to zero
   229  		copy = config.Config.FrontendServer.RightCopy
   230  		print = config.Config.FrontendServer.RightPrint
   231  		userRights := license.UserRights{}
   232  		userRights.Copy = &copy
   233  		userRights.Print = &print
   234  
   235  		// if this is a loan, include start and end dates from the purchase info
   236  		if purchase.Type == LOAN {
   237  			userRights.Start = purchase.StartDate
   238  			userRights.End = purchase.EndDate
   239  		}
   240  		partialLicense.Rights = &userRights
   241  	}
   242  
   243  	// encode in json
   244  	jsonBody, err := json.Marshal(partialLicense)
   245  	if err != nil {
   246  		return license.License{}, err
   247  	}
   248  
   249  	// get the url of the lcp server
   250  	lcpServerConfig := config.Config.LcpServer
   251  	var lcpURL string
   252  
   253  	if purchase.LicenseUUID == nil {
   254  		// if the purchase contains no license id, generate a new license
   255  		lcpURL = lcpServerConfig.PublicBaseUrl + "/contents/" + purchase.Publication.UUID + "/license"
   256  	} else {
   257  		// if the purchase contains a license id, fetch an existing license
   258  		// note: this will not update the license rights
   259  		lcpURL = lcpServerConfig.PublicBaseUrl + "/licenses/" + *purchase.LicenseUUID
   260  	}
   261  	// message to the console
   262  	log.Println("POST " + lcpURL)
   263  
   264  	// add the partial license to the POST request
   265  	req, err := http.NewRequest("POST", lcpURL, bytes.NewReader(jsonBody))
   266  	if err != nil {
   267  		return license.License{}, err
   268  	}
   269  	lcpUpdateAuth := config.Config.LcpUpdateAuth
   270  	if config.Config.LcpUpdateAuth.Username != "" {
   271  		req.SetBasicAuth(lcpUpdateAuth.Username, lcpUpdateAuth.Password)
   272  	}
   273  	// the body is a partial license in json format
   274  	req.Header.Add("Content-Type", api.ContentType_LCP_JSON)
   275  
   276  	var lcpClient = &http.Client{
   277  		Timeout: time.Second * 10,
   278  	}
   279  	// POST the request
   280  	resp, err := lcpClient.Do(req)
   281  	if err != nil {
   282  		return license.License{}, err
   283  	}
   284  
   285  	defer resp.Body.Close()
   286  
   287  	// if the status code from the request to the lcp server
   288  	// is neither 201 Created or 200 ok, return an internal error
   289  	if (purchase.LicenseUUID == nil && resp.StatusCode != 201) ||
   290  		(purchase.LicenseUUID != nil && resp.StatusCode != 200) {
   291  		return license.License{}, errors.New("the License Server returned an error")
   292  	}
   293  
   294  	// decode the full license
   295  	fullLicense := license.License{}
   296  	dec := json.NewDecoder(resp.Body)
   297  	err = dec.Decode(&fullLicense)
   298  
   299  	if err != nil {
   300  		return license.License{}, errors.New("unable to decode license")
   301  	}
   302  
   303  	// store the license id if it was not already set
   304  	if purchase.LicenseUUID == nil {
   305  		purchase.LicenseUUID = &fullLicense.ID
   306  		pManager.Update(purchase)
   307  		if err != nil {
   308  			return license.License{}, errors.New("unable to update the license id")
   309  		}
   310  	}
   311  
   312  	return fullLicense, nil
   313  }
   314  
   315  // GetPartialLicense gets the license associated with a purchase, from the license server
   316  func (pManager PurchaseManager) GetPartialLicense(purchase Purchase) (license.License, error) {
   317  
   318  	if purchase.LicenseUUID == nil {
   319  		return license.License{}, errors.New("no license has been yet delivered")
   320  	}
   321  
   322  	lcpServerConfig := config.Config.LcpServer
   323  	lcpURL := lcpServerConfig.PublicBaseUrl + "/licenses/" + *purchase.LicenseUUID
   324  	// message to the console
   325  	log.Println("GET " + lcpURL)
   326  	// prepare the request
   327  	req, err := http.NewRequest("GET", lcpURL, nil)
   328  	if err != nil {
   329  		return license.License{}, err
   330  	}
   331  	// set credentials
   332  	lcpUpdateAuth := config.Config.LcpUpdateAuth
   333  	if config.Config.LcpUpdateAuth.Username != "" {
   334  		req.SetBasicAuth(lcpUpdateAuth.Username, lcpUpdateAuth.Password)
   335  	}
   336  	// send the request
   337  	var lcpClient = &http.Client{
   338  		Timeout: time.Second * 10,
   339  	}
   340  	resp, err := lcpClient.Do(req)
   341  	if err != nil {
   342  		return license.License{}, err
   343  	}
   344  	defer resp.Body.Close()
   345  
   346  	// the call must return 206 (partial content) because there is no input partial license
   347  	if resp.StatusCode != 206 {
   348  		// bad status code
   349  		return license.License{}, errors.New("the License Server returned an error")
   350  	}
   351  	// decode the license
   352  	partialLicense := license.License{}
   353  	dec := json.NewDecoder(resp.Body)
   354  	err = dec.Decode(&partialLicense)
   355  
   356  	if err != nil {
   357  		return license.License{}, errors.New("unable to decode the license")
   358  	}
   359  
   360  	return partialLicense, nil
   361  }
   362  
   363  // GetLicenseStatusDocument gets a license status document associated with a purchase
   364  func (pManager PurchaseManager) GetLicenseStatusDocument(purchase Purchase) (licensestatuses.LicenseStatus, error) {
   365  	if purchase.LicenseUUID == nil {
   366  		return licensestatuses.LicenseStatus{}, errors.New("no license has been yet delivered")
   367  	}
   368  
   369  	lsdServerConfig := config.Config.LsdServer
   370  	lsdURL := lsdServerConfig.PublicBaseUrl + "/licenses/" + *purchase.LicenseUUID + "/status"
   371  	log.Println("GET " + lsdURL)
   372  	req, err := http.NewRequest("GET", lsdURL, nil)
   373  	if err != nil {
   374  		return licensestatuses.LicenseStatus{}, err
   375  	}
   376  	req.Header.Add("Content-Type", api.ContentType_JSON)
   377  
   378  	var lsdClient = &http.Client{
   379  		Timeout: time.Second * 10,
   380  	}
   381  
   382  	resp, err := lsdClient.Do(req)
   383  	if err != nil {
   384  		return licensestatuses.LicenseStatus{}, err
   385  	}
   386  
   387  	if resp.StatusCode != 200 {
   388  		// Bad status code
   389  		return licensestatuses.LicenseStatus{}, errors.New("the License Status Document server returned an error")
   390  	}
   391  
   392  	// Decode status document
   393  	statusDocument := licensestatuses.LicenseStatus{}
   394  	dec := json.NewDecoder(resp.Body)
   395  	err = dec.Decode(&statusDocument)
   396  
   397  	if err != nil {
   398  		return licensestatuses.LicenseStatus{}, err
   399  	}
   400  
   401  	defer resp.Body.Close()
   402  
   403  	return statusDocument, nil
   404  }
   405  
   406  // List all purchases, with pagination
   407  func (pManager PurchaseManager) List(page int, pageNum int) func() (Purchase, error) {
   408  
   409  	var rows *sql.Rows
   410  	var err error
   411  	driver, _ := config.GetDatabase(config.Config.FrontendServer.Database)
   412  	if driver == "mssql" {
   413  		rows, err = pManager.dbList.Query(pageNum*page, page)
   414  	} else {
   415  		rows, err = pManager.dbList.Query(page, pageNum*page)
   416  	}
   417  	if err != nil {
   418  		log.Printf("Failed to get the full list of purchases: %s", err.Error())
   419  	}
   420  	return convertRecordsToPurchases(rows)
   421  }
   422  
   423  // ListByUser lists the purchases of a given user, with pagination
   424  func (pManager PurchaseManager) ListByUser(userID int64, page int, pageNum int) func() (Purchase, error) {
   425  
   426  	var rows *sql.Rows
   427  	var err error
   428  	driver, _ := config.GetDatabase(config.Config.FrontendServer.Database)
   429  	if driver == "mssql" {
   430  		rows, err = pManager.dbListByUser.Query(userID, pageNum*page, page)
   431  	} else {
   432  		rows, err = pManager.dbListByUser.Query(userID, page, pageNum*page)
   433  	}
   434  	if err != nil {
   435  		log.Printf("Failed to get the user list of purchases: %s", err.Error())
   436  	}
   437  	return convertRecordsToPurchases(rows)
   438  }
   439  
   440  // Add a purchase
   441  func (pManager PurchaseManager) Add(p Purchase) error {
   442  
   443  	// Fill default values
   444  	if p.TransactionDate.IsZero() {
   445  		p.TransactionDate = time.Now().UTC().Truncate(time.Second)
   446  	}
   447  
   448  	if p.Type == LOAN && p.StartDate == nil {
   449  		p.StartDate = &p.TransactionDate
   450  	}
   451  
   452  	// Create uuid
   453  	uid, err_u := uuid.NewV4()
   454  	if err_u != nil {
   455  		return err_u
   456  	}
   457  	p.UUID = uid.String()
   458  
   459  	_, err := pManager.db.Exec(dbutils.GetParamQuery(config.Config.FrontendServer.Database, `INSERT INTO purchase
   460  	(uuid, publication_id, user_id, type, transaction_date, start_date, end_date, status)
   461  	VALUES (?, ?, ?, ?, ?, ?, ?, 'ok')`),
   462  		p.UUID, p.Publication.ID, p.User.ID, string(p.Type), p.TransactionDate, p.StartDate, p.EndDate)
   463  
   464  	return err
   465  }
   466  
   467  // Update modifies a purchase on a renew or return request
   468  // parameters: a Purchase structure withID,	LicenseUUID, StartDate,	EndDate, Status
   469  // EndDate may be undefined (nil), in which case the lsd server will choose the renew period
   470  func (pManager PurchaseManager) Update(p Purchase) error {
   471  	// Get the original purchase from the db
   472  	origPurchase, err := pManager.Get(p.ID)
   473  
   474  	if err != nil {
   475  		return ErrNotFound
   476  	}
   477  	if origPurchase.Status != StatusOk {
   478  		return errors.New("cannot update an invalid purchase")
   479  	}
   480  	if p.Status == StatusToBeRenewed ||
   481  		p.Status == StatusToBeReturned {
   482  
   483  		if p.LicenseUUID == nil {
   484  			return errors.New("cannot return or renew a purchase when no license has been delivered")
   485  		}
   486  
   487  		lsdServerConfig := config.Config.LsdServer
   488  		lsdURL := lsdServerConfig.PublicBaseUrl + "/licenses/" + *p.LicenseUUID
   489  
   490  		if p.Status == StatusToBeRenewed {
   491  			lsdURL += "/renew"
   492  
   493  			if p.EndDate != nil {
   494  				lsdURL += "?end=" + p.EndDate.Format(time.RFC3339)
   495  			}
   496  
   497  			// Next status if LSD raises no error
   498  			p.Status = StatusOk
   499  		} else if p.Status == StatusToBeReturned {
   500  			lsdURL += "/return"
   501  
   502  			// Next status if LSD raises no error
   503  			p.Status = StatusOk
   504  		}
   505  		// message to the console
   506  		log.Println("PUT " + lsdURL)
   507  		// prepare the request for renew or return to the license status server
   508  		req, err := http.NewRequest("PUT", lsdURL, nil)
   509  		if err != nil {
   510  			return err
   511  		}
   512  		// set credentials
   513  		lsdAuth := config.Config.LsdNotifyAuth
   514  		if lsdAuth.Username != "" {
   515  			req.SetBasicAuth(lsdAuth.Username, lsdAuth.Password)
   516  		}
   517  		// call the lsd server
   518  		var lsdClient = &http.Client{
   519  			Timeout: time.Second * 10,
   520  		}
   521  		resp, err := lsdClient.Do(req)
   522  		if err != nil {
   523  			return err
   524  		}
   525  		defer resp.Body.Close()
   526  
   527  		// if the renew/return request was refused by the License Status server
   528  		if resp.StatusCode != 200 {
   529  			return ErrNoChange
   530  		}
   531  
   532  		// get the new end date from the license server
   533  		// FIXME: is there a lighter solution to get the new end date?
   534  		license, err := pManager.GetPartialLicense(origPurchase)
   535  		if err != nil {
   536  			return err
   537  		}
   538  		p.EndDate = license.Rights.End
   539  	} else {
   540  		// status is not "to be renewed"
   541  		p.Status = StatusOk
   542  	}
   543  	// update the db with the updated license id, start date, end date, status
   544  	result, err := pManager.db.Exec(dbutils.GetParamQuery(config.Config.FrontendServer.Database, `UPDATE purchase SET license_uuid=?, start_date=?, end_date=?, status=? WHERE id=?`),
   545  		p.LicenseUUID, p.StartDate, p.EndDate, p.Status, p.ID)
   546  	if err != nil {
   547  		return err
   548  	}
   549  	if changed, err := result.RowsAffected(); err == nil {
   550  		if changed != 1 {
   551  			return ErrNoChange
   552  		}
   553  	}
   554  	return err
   555  }
   556  
   557  // Init initializes the PurchaseManager
   558  func Init(db *sql.DB) (i WebPurchase, err error) {
   559  
   560  	driver, _ := config.GetDatabase(config.Config.FrontendServer.Database)
   561  
   562  	// if sqlite, create the content table in the frontend db if it does not exist
   563  	if driver == "sqlite3" {
   564  		_, err = db.Exec(tableDef)
   565  		if err != nil {
   566  			log.Println("Error creating purchase table")
   567  			return
   568  		}
   569  	}
   570  
   571  	selectQuery := `SELECT p.id, p.uuid, p.type, p.transaction_date, p.license_uuid, p.start_date, p.end_date, p.status,
   572  	u.id, u.uuid, u.name, u.email, u.password, u.hint,
   573  	pu.id, pu.uuid, pu.title, pu.status
   574  	FROM purchase p JOIN "user" u ON (p.user_id=u.id) JOIN publication pu ON (p.publication_id=pu.id)`
   575  
   576  	var dbGetByID *sql.Stmt
   577  	dbGetByID, err = db.Prepare(selectQuery + dbutils.GetParamQuery(config.Config.FrontendServer.Database, ` WHERE p.id = ?`))
   578  	if err != nil {
   579  		return
   580  	}
   581  
   582  	var dbGetByLicenseID *sql.Stmt
   583  	dbGetByLicenseID, err = db.Prepare(selectQuery + dbutils.GetParamQuery(config.Config.FrontendServer.Database, ` WHERE p.license_uuid = ?`))
   584  	if err != nil {
   585  		return
   586  	}
   587  
   588  	var dbList *sql.Stmt
   589  	if driver == "mssql" {
   590  		dbList, err = db.Prepare(selectQuery + ` ORDER BY p.transaction_date desc OFFSET ? ROWS FETCH NEXT ? ROWS ONLY`)
   591  	} else {
   592  		dbList, err = db.Prepare(selectQuery + dbutils.GetParamQuery(config.Config.FrontendServer.Database, ` ORDER BY p.transaction_date desc LIMIT ? OFFSET ?`))
   593  	}
   594  	if err != nil {
   595  		return
   596  	}
   597  
   598  	var dbListByUser *sql.Stmt
   599  	if driver == "mssql" {
   600  		dbListByUser, err = db.Prepare(selectQuery + ` WHERE u.id = ? ORDER BY p.transaction_date desc OFFSET ? ROWS FETCH NEXT ? ROWS ONLY`)
   601  	} else {
   602  		dbListByUser, err = db.Prepare(selectQuery + dbutils.GetParamQuery(config.Config.FrontendServer.Database, ` WHERE u.id = ? ORDER BY p.transaction_date desc LIMIT ? OFFSET ?`))
   603  	}
   604  	if err != nil {
   605  		return
   606  	}
   607  
   608  	i = PurchaseManager{db, dbGetByID, dbGetByLicenseID, dbList, dbListByUser}
   609  	return
   610  }
   611  
   612  const tableDef = "CREATE TABLE IF NOT EXISTS purchase (" +
   613  	"id integer NOT NULL PRIMARY KEY," +
   614  	"uuid varchar(255) NOT NULL," +
   615  	"publication_id integer NOT NULL," +
   616  	"user_id integer NOT NULL," +
   617  	"license_uuid varchar(255) NULL," +
   618  	"type varchar(32) NOT NULL," +
   619  	"transaction_date datetime," +
   620  	"start_date datetime," +
   621  	"end_date datetime," +
   622  	"status varchar(255) NOT NULL," +
   623  	"FOREIGN KEY (publication_id) REFERENCES publication(id)," +
   624  	"FOREIGN KEY (user_id) REFERENCES user(id)" +
   625  	");" +
   626  	"CREATE INDEX IF NOT EXISTS idx_purchase ON purchase (license_uuid)"