557 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			557 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package training
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"github.com/jackc/pgx/v5"
 | |
| 	"github.com/jackc/pgx/v5/pgxpool"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| type PostgresTrainingRepository struct {
 | |
| 	pg *pgxpool.Pool
 | |
| }
 | |
| 
 | |
| func NewPostgresTrainingRepository(pg *pgxpool.Pool) *PostgresTrainingRepository {
 | |
| 	return &PostgresTrainingRepository{pg: pg}
 | |
| }
 | |
| 
 | |
| func (r *PostgresTrainingRepository) Create(training *Training) error {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	tx, txErr := r.pg.Begin(ctx)
 | |
| 	if txErr != nil {
 | |
| 		return txErr
 | |
| 	}
 | |
| 
 | |
| 	queryErr := tx.QueryRow(ctx, `
 | |
| 		INSERT INTO training.trainings (name, description, days)
 | |
| 		VALUES ($1, $2, $3)
 | |
| 		RETURNING id
 | |
| 	`, training.Name, training.Description, training.Days).Scan(&training.ID)
 | |
| 	if queryErr != nil {
 | |
| 		return queryErr
 | |
| 	}
 | |
| 
 | |
| 	queryBatch := &pgx.Batch{}
 | |
| 
 | |
| 	for _, price := range training.Pricing {
 | |
| 		queryBatch.Queue(`
 | |
| 			INSERT INTO training.prices (training_id, amount, currency, type)
 | |
| 			VALUES ($1, $2, $3, $4)
 | |
| 		`, training.ID, price.Amount, price.Currency, price.Type)
 | |
| 	}
 | |
| 
 | |
| 	var err error
 | |
| 	batch := tx.SendBatch(ctx, queryBatch)
 | |
| 	defer func() {
 | |
| 		if e := batch.Close(); e != nil {
 | |
| 			err = e
 | |
| 		}
 | |
| 
 | |
| 		if err != nil {
 | |
| 			_ = tx.Rollback(ctx)
 | |
| 		} else {
 | |
| 			if e := tx.Commit(ctx); e != nil {
 | |
| 				err = e
 | |
| 			}
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	_, err = batch.Exec()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresTrainingRepository) FindByID(id ID) (*Training, error) {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	var training Training
 | |
| 	err := r.pg.QueryRow(ctx, `
 | |
| 		SELECT id, name, description, days
 | |
| 		FROM training.trainings
 | |
| 		WHERE id = $1
 | |
| 	`, id).Scan(&training.ID, &training.Name, &training.Description, &training.Days)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	rows, err := r.pg.Query(ctx, `
 | |
| 		SELECT amount, currency, type
 | |
| 		FROM training.prices
 | |
| 		WHERE training_id = $1
 | |
| 	`, id)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer rows.Close()
 | |
| 
 | |
| 	training.Pricing = make([]Price, 0)
 | |
| 	training.Pricing, err = pgx.CollectRows[Price](rows, func(row pgx.CollectableRow) (Price, error) {
 | |
| 		var price Price
 | |
| 		err := row.Scan(&price.Amount, &price.Currency, &price.Type)
 | |
| 		if err != nil {
 | |
| 			return Price{}, err
 | |
| 		}
 | |
| 		return price, nil
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return &training, nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresTrainingRepository) FindAll() ([]Training, error) {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	rows, err := r.pg.Query(ctx, `
 | |
| 		SELECT *
 | |
| 		FROM training.trainings
 | |
| 	`)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer rows.Close()
 | |
| 
 | |
| 	trainings, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (Training, error) {
 | |
| 		var training Training
 | |
| 		scanErr := row.Scan(&training.ID, &training.Name, &training.Description, &training.Days)
 | |
| 		if scanErr != nil {
 | |
| 			return Training{}, scanErr
 | |
| 		}
 | |
| 
 | |
| 		priceRows, queryErr := r.pg.Query(ctx, `SELECT amount, currency, type FROM training.prices WHERE training_id = $1`, training.ID)
 | |
| 		if queryErr != nil {
 | |
| 			return Training{}, queryErr
 | |
| 		}
 | |
| 
 | |
| 		training.Pricing, scanErr = pgx.CollectRows(priceRows, pgx.RowToStructByName[Price])
 | |
| 		if scanErr != nil {
 | |
| 			return Training{}, scanErr
 | |
| 		}
 | |
| 
 | |
| 		return training, nil
 | |
| 	})
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return trainings, nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresTrainingRepository) Update(training *Training) error {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	tx, err := r.pg.Begin(ctx)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	_, err = tx.Exec(ctx, `
 | |
| 		UPDATE training.trainings
 | |
| 		SET name = $1, description = $2, days = $3
 | |
| 		WHERE id = $4
 | |
| 	`, training.Name, training.Description, training.Days, training.ID)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	queryBatch := &pgx.Batch{}
 | |
| 
 | |
| 	for _, price := range training.Pricing {
 | |
| 		queryBatch.Queue(`
 | |
| 			INSERT INTO training.prices (training_id, amount, currency, type)
 | |
| 			VALUES ($1, $2, $3, $4)
 | |
| 		`, training.ID, price.Amount, price.Currency, price.Type)
 | |
| 	}
 | |
| 
 | |
| 	batch := tx.SendBatch(ctx, queryBatch)
 | |
| 	defer func() {
 | |
| 		if e := batch.Close(); e != nil {
 | |
| 			err = e
 | |
| 		}
 | |
| 
 | |
| 		if err != nil {
 | |
| 			_ = tx.Rollback(ctx)
 | |
| 		} else {
 | |
| 			if e := tx.Commit(ctx); e != nil {
 | |
| 				err = e
 | |
| 			}
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	_, err = batch.Exec()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresTrainingRepository) Delete(id ID) error {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	tx, err := r.pg.Begin(ctx)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	_, err = tx.Exec(context.Background(), `
 | |
| 		DELETE FROM training.prices
 | |
| 		WHERE training_id = $1
 | |
| 	`, id)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	_, err = tx.Exec(context.Background(), `
 | |
| 		DELETE FROM training.trainings
 | |
| 		WHERE id = $1
 | |
| 	`, id)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	err = tx.Commit(ctx)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| type PostgresDateRepository struct {
 | |
| 	pg *pgxpool.Pool
 | |
| }
 | |
| 
 | |
| func NewPostgresDateRepository(pg *pgxpool.Pool) *PostgresDateRepository {
 | |
| 	return &PostgresDateRepository{pg: pg}
 | |
| }
 | |
| 
 | |
| func (r *PostgresDateRepository) Create(trainingID ID, trainingDate *Date) error {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	err := r.pg.QueryRow(ctx, `
 | |
| 		INSERT INTO training.dates (training_id, date, start_time, days, is_online, location, address, capacity, price_amount, price_currency)
 | |
| 		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`,
 | |
| 		trainingID,
 | |
| 		trainingDate.Date,
 | |
| 		trainingDate.StartTime,
 | |
| 		trainingDate.Days,
 | |
| 		trainingDate.IsOnline,
 | |
| 		trainingDate.Location,
 | |
| 		trainingDate.Address,
 | |
| 		trainingDate.Capacity,
 | |
| 		trainingDate.PriceAmount,
 | |
| 		trainingDate.PriceCurrency,
 | |
| 	).Scan(&trainingDate.ID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresDateRepository) FindByID(id DateID) (*Date, error) {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	var trainingDate Date
 | |
| 	err := r.pg.QueryRow(ctx, `SELECT * FROM training.dates WHERE id = $1`, id).
 | |
| 		Scan(
 | |
| 			&trainingDate.ID,
 | |
| 			&trainingDate.trainingID,
 | |
| 			&trainingDate.Date,
 | |
| 			&trainingDate.StartTime,
 | |
| 			&trainingDate.Days,
 | |
| 			&trainingDate.IsOnline,
 | |
| 			&trainingDate.Location,
 | |
| 			&trainingDate.Address,
 | |
| 			&trainingDate.Capacity,
 | |
| 			&trainingDate.PriceAmount,
 | |
| 			&trainingDate.PriceCurrency,
 | |
| 		)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return &trainingDate, nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresDateRepository) FindAll() ([]Date, error) {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	rows, err := r.pg.Query(ctx, `SELECT * FROM training.dates`)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer rows.Close()
 | |
| 
 | |
| 	var trainingDates []Date
 | |
| 	for rows.Next() {
 | |
| 		var trainingDate Date
 | |
| 		err := rows.Scan(
 | |
| 			&trainingDate.ID,
 | |
| 			&trainingDate.trainingID,
 | |
| 			&trainingDate.Date,
 | |
| 			&trainingDate.StartTime,
 | |
| 			&trainingDate.Days,
 | |
| 			&trainingDate.IsOnline,
 | |
| 			&trainingDate.Location,
 | |
| 			&trainingDate.Address,
 | |
| 			&trainingDate.Capacity,
 | |
| 			&trainingDate.PriceAmount,
 | |
| 			&trainingDate.PriceCurrency,
 | |
| 		)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		trainingDates = append(trainingDates, trainingDate)
 | |
| 	}
 | |
| 
 | |
| 	return trainingDates, nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresDateRepository) FindAllByTrainingID(trainingID ID) ([]Date, error) {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	rows, err := r.pg.Query(ctx, `SELECT * FROM training.dates WHERE training_id = $1`, trainingID)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer rows.Close()
 | |
| 
 | |
| 	var trainingDates []Date
 | |
| 	for rows.Next() {
 | |
| 		var trainingDate Date
 | |
| 		err := rows.Scan(
 | |
| 			&trainingDate.ID,
 | |
| 			&trainingDate.trainingID,
 | |
| 			&trainingDate.Date,
 | |
| 			&trainingDate.StartTime,
 | |
| 			&trainingDate.Days,
 | |
| 			&trainingDate.IsOnline,
 | |
| 			&trainingDate.Location,
 | |
| 			&trainingDate.Address,
 | |
| 			&trainingDate.Capacity,
 | |
| 			&trainingDate.PriceAmount,
 | |
| 			&trainingDate.PriceCurrency,
 | |
| 		)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		trainingDates = append(trainingDates, trainingDate)
 | |
| 	}
 | |
| 
 | |
| 	return trainingDates, nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresDateRepository) FindUpcomingByTrainingID(trainingID ID) ([]Date, error) {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	rows, err := r.pg.Query(ctx, `
 | |
| 		SELECT * FROM training.dates
 | |
| 		WHERE training_id = $1 AND date > $2
 | |
| 	`, trainingID, time.Now())
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer rows.Close()
 | |
| 
 | |
| 	var trainingDates []Date
 | |
| 	for rows.Next() {
 | |
| 		var trainingDate Date
 | |
| 		err := rows.Scan(
 | |
| 			&trainingDate.ID,
 | |
| 			&trainingDate.trainingID,
 | |
| 			&trainingDate.Date,
 | |
| 			&trainingDate.StartTime,
 | |
| 			&trainingDate.Days,
 | |
| 			&trainingDate.IsOnline,
 | |
| 			&trainingDate.Location,
 | |
| 			&trainingDate.Address,
 | |
| 			&trainingDate.Capacity,
 | |
| 			&trainingDate.PriceAmount,
 | |
| 			&trainingDate.PriceCurrency,
 | |
| 		)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		trainingDates = append(trainingDates, trainingDate)
 | |
| 	}
 | |
| 
 | |
| 	return trainingDates, nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresDateRepository) Update(trainingDate *Date) error {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	_, err := r.pg.Exec(ctx, `
 | |
| 		UPDATE training.dates
 | |
| 		SET date = $1, start_time = $2, days = $3, is_online = $4, location = $5, address = $6, capacity = $7, price_amount = $8, price_currency = $9
 | |
| 		WHERE id = $10
 | |
| 	`, trainingDate.Date, trainingDate.StartTime, trainingDate.Days, trainingDate.IsOnline, trainingDate.Location, trainingDate.Address, trainingDate.Capacity, trainingDate.PriceAmount, trainingDate.PriceCurrency, trainingDate.ID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresDateRepository) Delete(id DateID) error {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	_, err := r.pg.Exec(ctx, `DELETE FROM training.dates WHERE id = $1`, id)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| type PostgresAttendeeRepository struct {
 | |
| 	pg *pgxpool.Pool
 | |
| }
 | |
| 
 | |
| func NewPostgresAttendeeRepository(pg *pgxpool.Pool) *PostgresAttendeeRepository {
 | |
| 	return &PostgresAttendeeRepository{pg: pg}
 | |
| }
 | |
| 
 | |
| func (r *PostgresAttendeeRepository) Create(trainingDateID DateID, attendee *Attendee) error {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	err := r.pg.QueryRow(ctx, `
 | |
| 		INSERT INTO training.attendees (date_id, name, email, phone, company, position, is_student, has_paid, has_attended, bill_amount, bill_currency)
 | |
| 		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
 | |
| 		RETURNING id
 | |
| 	`, trainingDateID, attendee.Name, attendee.Email, attendee.Phone, attendee.Company, attendee.Position, attendee.IsStudent, attendee.HasPaid, attendee.HasAttended, attendee.BillAmount, attendee.BillCurrency).
 | |
| 		Scan(&attendee.ID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresAttendeeRepository) FindByID(id AttendeeID) (*Attendee, error) {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	var attendee Attendee
 | |
| 	err := r.pg.QueryRow(ctx, `
 | |
| 		SELECT id, date_id, name, email, phone, company, position, is_student, has_paid, has_attended, bill_amount, bill_currency
 | |
| 		FROM training.attendees
 | |
| 		WHERE id = $1
 | |
| 	`, id).Scan(&attendee.ID, &attendee.trainingDateID, &attendee.Name, &attendee.Email, &attendee.Phone, &attendee.Company, &attendee.Position, &attendee.IsStudent, &attendee.HasPaid, &attendee.HasAttended, &attendee.BillAmount, &attendee.BillCurrency)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return &attendee, nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresAttendeeRepository) FindAll() ([]Attendee, error) {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	rows, err := r.pg.Query(ctx, `
 | |
| 		SELECT id, date_id, name, email, phone, company, position, is_student, has_paid, has_attended, bill_amount, bill_currency
 | |
| 		FROM training.attendees
 | |
| 	`)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer rows.Close()
 | |
| 
 | |
| 	var attendees []Attendee
 | |
| 	for rows.Next() {
 | |
| 		var attendee Attendee
 | |
| 		err := rows.Scan(&attendee.ID, &attendee.trainingDateID, &attendee.Name, &attendee.Email, &attendee.Phone, &attendee.Company, &attendee.Position, &attendee.IsStudent, &attendee.HasPaid, &attendee.HasAttended, &attendee.BillAmount, &attendee.BillCurrency)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		attendees = append(attendees, attendee)
 | |
| 	}
 | |
| 
 | |
| 	return attendees, nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresAttendeeRepository) FindAllByTrainingDateID(trainingDateID DateID) ([]Attendee, error) {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	rows, err := r.pg.Query(ctx, `
 | |
| 		SELECT id, date_id, name, email, phone, company, position, is_student, has_paid, has_attended, bill_amount, bill_currency
 | |
| 		FROM training.attendees
 | |
| 		WHERE date_id = $1
 | |
| 	`, trainingDateID)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer rows.Close()
 | |
| 
 | |
| 	var attendees []Attendee
 | |
| 	for rows.Next() {
 | |
| 		var attendee Attendee
 | |
| 		err := rows.Scan(&attendee.ID, &attendee.trainingDateID, &attendee.Name, &attendee.Email, &attendee.Phone, &attendee.Company, &attendee.Position, &attendee.IsStudent, &attendee.HasPaid, &attendee.HasAttended, &attendee.BillAmount, &attendee.BillCurrency)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		attendees = append(attendees, attendee)
 | |
| 	}
 | |
| 
 | |
| 	return attendees, nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresAttendeeRepository) Update(attendee *Attendee) error {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	_, err := r.pg.Exec(ctx, `
 | |
| 		UPDATE training.attendees
 | |
| 		SET name = $1, email = $2, phone = $3, company = $4, position = $5, is_student = $6, has_paid = $7, has_attended = $8, bill_amount = $9, bill_currency = $10
 | |
| 		WHERE id = $10
 | |
| 	`, attendee.Name, attendee.Email, attendee.Phone, attendee.Company, attendee.Position, attendee.IsStudent, attendee.HasPaid, attendee.HasAttended, attendee.BillAmount, attendee.BillCurrency, attendee.ID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (r *PostgresAttendeeRepository) Delete(id AttendeeID) error {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	_, err := r.pg.Exec(ctx, `DELETE FROM training.attendees WHERE id = $1`, id)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 |