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